$themedesignermode = !empty($CFG->themedesignermode);
// Check if a directory with development libraries exists.
-if (is_dir($CFG->dirroot.'/vendor') || is_dir($CFG->dirroot.'/node_modules')) {
+if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || is_dir($CFG->dirroot.'/node_modules'))) {
$devlibdir = true;
} else {
$devlibdir = false;
require_once('../config.php');
require_once($CFG->libdir.'/adminlib.php');
+redirect_if_major_upgrade_required();
+
$query = trim(optional_param('query', '', PARAM_NOTAGS)); // Search string
$context = context_system::instance();
$PAGE->set_context($context);
+$hassiteconfig = has_capability('moodle/site:config', $context);
+
+if ($hassiteconfig && moodle_needs_upgrading()) {
+ redirect(new moodle_url('/admin/index.php'));
+}
+
admin_externalpage_setup('search', '', array('query' => $query)); // now hidden page
$adminroot = admin_get_root(); // need all settings here
$showsettingslinks = true;
-if (has_capability('moodle/site:config', $context)) {
+if ($hassiteconfig) {
require_once("admin_settings_search_form.php");
$form = new admin_settings_search_form();
$form->display();
$temp->add(new admin_setting_heading('searchengineheading', new lang_string('searchengine', 'admin'), ''));
$temp->add(new admin_setting_configselect('searchengine',
new lang_string('selectsearchengine', 'admin'), '', 'solr', $engines));
- $temp->add(new admin_setting_heading('searchindexingheading', new lang_string('searchoptions', 'admin'), ''));
+ $temp->add(new admin_setting_heading('searchoptionsheading', new lang_string('searchoptions', 'admin'), ''));
$temp->add(new admin_setting_configcheckbox('searchindexwhendisabled',
new lang_string('searchindexwhendisabled', 'admin'), new lang_string('searchindexwhendisabled_desc', 'admin'),
0));
$temp->add(new admin_setting_configduration('searchindextime',
new lang_string('searchindextime', 'admin'), new lang_string('searchindextime_desc', 'admin'),
600));
+ $options = [
+ 0 => new lang_string('searchallavailablecourses_off', 'admin'),
+ 1 => new lang_string('searchallavailablecourses_on', 'admin')
+ ];
+ $temp->add(new admin_setting_configselect('searchallavailablecourses',
+ new lang_string('searchallavailablecourses', 'admin'),
+ new lang_string('searchallavailablecourses_desc', 'admin'),
+ 0, $options));
$ADMIN->add('searchplugins', $temp);
$ADMIN->add('searchplugins', new admin_externalpage('searchareas', new lang_string('searchareas', 'admin'),
$mform = $this->_form;
- if ($this->_customdata['model']->get_model_obj()->trained == 1) {
+ if ($this->_customdata['model']->is_trained()) {
$message = get_string('edittrainedwarning', 'tool_analytics');
$mform->addElement('html', $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING));
}
global $CFG;
require_once($CFG->libdir . '/adminlib.php');
+ $cachekey = 'mobileplugins';
+ if (!isloggedin()) {
+ $cachekey = 'authmobileplugins'; // Use a different cache for not logged users.
+ }
+
// Check if we can return this from cache.
$cache = \cache::make('tool_mobile', 'plugininfo');
- $pluginsinfo = $cache->get('mobileplugins');
+ $pluginsinfo = $cache->get($cachekey);
if ($pluginsinfo !== false) {
return (array)$pluginsinfo;
}
$pluginsinfo = [];
- $plugintypes = core_component::get_plugin_types();
+ // For not logged users return only auth plugins.
+ // This is to avoid anyone (not being a registered user) to obtain and download all the site remote add-ons.
+ if (!isloggedin()) {
+ $plugintypes = array('auth' => $CFG->dirroot.'/auth');
+ } else {
+ $plugintypes = core_component::get_plugin_types();
+ }
foreach ($plugintypes as $plugintype => $unused) {
// We need to include files here.
}
}
- $cache->set('mobileplugins', $pluginsinfo);
+ $cache->set($cachekey, $pluginsinfo);
return $pluginsinfo;
}
*/
namespace tool_mobile;
+defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/externallib.php");
'methodname' => 'get_plugins_supporting_mobile',
'description' => 'Returns a list of Moodle plugins supporting the mobile app.',
'type' => 'read',
- 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+ 'ajax' => true,
+ 'loginrequired' => false,
),
'tool_mobile_get_public_config' => array(
This files describes changes in tool_mobile code.
Information provided here is intended especially for developers.
+=== 3.4 ===
+
+ * External function tool_mobile::tool_mobile_get_plugins_supporting_mobile is now available via AJAX for not logged users.
+ When called via AJAX without a user session the function will return only auth plugins.
+
=== 3.3 ===
* External function tool_mobile::get_public_config now returns the mobilecssurl field (Mobile custom CSS theme).
*/
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2017051500; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2017051501; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2017050500; // Requires this Moodle version.
$plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
$plugin->dependencies = array(
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_templatelibrary;
+defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/externallib.php");
$this->log = array();
}
+ /**
+ * Returns the list of analysable elements available on the site.
+ *
+ * \core_analytics\local\analyser\by_course and \core_analytics\local\analyser\sitewide are implementing
+ * this method returning site courses (by_course) and the whole system (sitewide) as analysables.
+ *
+ * @return \core_analytics\analysable[]
+ */
+ abstract public function get_analysables();
+
/**
* This function returns this analysable list of samples.
*
*
* @return string
*/
- abstract protected function get_samples_origin();
+ abstract public function get_samples_origin();
/**
* Returns the context of a sample.
/**
* Main analyser method which processes the site analysables.
*
- * \core_analytics\local\analyser\by_course and \core_analytics\local\analyser\sitewide are implementing
- * this method returning site courses (by_course) and the whole system (sitewide) as analysables.
- * In most of the cases you should have enough extending from one of these classes so you don't need
- * to reimplement this method.
- *
* @param bool $includetarget
* @return \stored_file[]
*/
- abstract public function get_analysable_data($includetarget);
+ public function get_analysable_data($includetarget) {
+
+ $filesbytimesplitting = array();
+
+ $analysables = $this->get_analysables();
+ foreach ($analysables as $analysable) {
+
+ $files = $this->process_analysable($analysable, $includetarget);
+
+ // Later we will need to aggregate data by time splitting method.
+ foreach ($files as $timesplittingid => $file) {
+ $filesbytimesplitting[$timesplittingid][$analysable->get_id()] = $file;
+ }
+ }
+
+ // We join the datasets by time splitting method.
+ $timesplittingfiles = $this->merge_analysable_files($filesbytimesplitting, $includetarget);
+
+ return $timesplittingfiles;
+ }
/**
* Samples data this analyser provides.
}
}
+ /**
+ * Merges analysable dataset files into 1.
+ *
+ * @param array $filesbytimesplitting
+ * @param bool $includetarget
+ * @return \stored_file[]
+ */
+ protected function merge_analysable_files($filesbytimesplitting, $includetarget) {
+
+ $timesplittingfiles = array();
+ foreach ($filesbytimesplitting as $timesplittingid => $files) {
+
+ if ($this->options['evaluation'] === true) {
+ // Delete the previous copy. Only when evaluating.
+ \core_analytics\dataset_manager::delete_previous_evaluation_file($this->modelid, $timesplittingid);
+ }
+
+ // Merge all course files into one.
+ if ($includetarget) {
+ $filearea = \core_analytics\dataset_manager::LABELLED_FILEAREA;
+ } else {
+ $filearea = \core_analytics\dataset_manager::UNLABELLED_FILEAREA;
+ }
+ $timesplittingfiles[$timesplittingid] = \core_analytics\dataset_manager::merge_datasets($files,
+ $this->modelid, $timesplittingid, $filearea, $this->options['evaluation']);
+ }
+
+ return $timesplittingfiles;
+ }
+
/**
* Checks that this analyser satisfies the provided indicator requirements.
*
*
* @return \core_analytics\course[]
*/
- public function get_courses() {
+ public function get_analysables() {
// Default to all system courses.
if (!empty($this->options['filter'])) {
$courses = $this->options['filter'];
} else {
// Iterate through all potentially valid courses.
- $courses = get_courses();
+ $courses = get_courses('all', 'c.sortorder ASC');
}
unset($courses[SITEID]);
foreach ($courses as $course) {
// Skip the frontpage course.
$analysable = \core_analytics\course::instance($course);
- $analysables[$analysable->get_id()] = $analysable;
+ $analysables[] = $analysable;
}
if (empty($analysables)) {
return $analysables;
}
-
- /**
- * Returns the analysed data
- *
- * @param bool $includetarget
- * @return \stored_file[]
- */
- public function get_analysable_data($includetarget) {
-
- $filesbytimesplitting = array();
-
- // This class and all children will iterate through a list of courses (\core_analytics\course).
- $analysables = $this->get_courses('all', 'c.sortorder ASC');
- foreach ($analysables as $analysableid => $analysable) {
-
- $files = $this->process_analysable($analysable, $includetarget);
-
- // Later we will need to aggregate data by time splitting method.
- foreach ($files as $timesplittingid => $file) {
- $filesbytimesplitting[$timesplittingid][$analysableid] = $file;
- }
- }
-
- // We join the datasets by time splitting method.
- $timesplittingfiles = $this->merge_analysable_files($filesbytimesplitting, $includetarget);
-
- return $timesplittingfiles;
- }
-
- /**
- * Merges analysable dataset files into 1.
- *
- * @param array $filesbytimesplitting
- * @param bool $includetarget
- * @return \stored_file[]
- */
- protected function merge_analysable_files($filesbytimesplitting, $includetarget) {
-
- $timesplittingfiles = array();
- foreach ($filesbytimesplitting as $timesplittingid => $files) {
-
- if ($this->options['evaluation'] === true) {
- // Delete the previous copy. Only when evaluating.
- \core_analytics\dataset_manager::delete_previous_evaluation_file($this->modelid, $timesplittingid);
- }
-
- // Merge all course files into one.
- if ($includetarget) {
- $filearea = \core_analytics\dataset_manager::LABELLED_FILEAREA;
- } else {
- $filearea = \core_analytics\dataset_manager::UNLABELLED_FILEAREA;
- }
- $timesplittingfiles[$timesplittingid] = \core_analytics\dataset_manager::merge_datasets($files,
- $this->modelid, $timesplittingid, $filearea, $this->options['evaluation']);
- }
-
- return $timesplittingfiles;
- }
}
abstract class sitewide extends base {
/**
- * Returns the analysable data.
+ * Returns one single analysable element, the site.
*
- * @param bool $includetarget
- * @return \stored_file[] One file for each time splitting method.
+ * @return \core_analytics\analysable[]
*/
- public function get_analysable_data($includetarget) {
-
- // Here there is a single analysable and it is the system.
+ public function get_analysables() {
$analysable = new \core_analytics\site();
-
- $files = $this->process_analysable($analysable, $includetarget);
-
- // Copy to range files as there is just one analysable.
- foreach ($files as $timesplittingid => $file) {
-
- if ($this->options['evaluation'] === true) {
- // Delete the previous copy. Only when evaluating.
- \core_analytics\dataset_manager::delete_previous_evaluation_file($this->modelid, $timesplittingid);
- }
-
- // We use merge but it is just a copy.
- if ($includetarget) {
- $filearea = \core_analytics\dataset_manager::LABELLED_FILEAREA;
- } else {
- $filearea = \core_analytics\dataset_manager::UNLABELLED_FILEAREA;
- }
- $files[$timesplittingid] = \core_analytics\dataset_manager::merge_datasets(array($file), $this->modelid,
- $timesplittingid, $filearea, $this->options['evaluation']);
- }
-
- return $files;
+ return array($analysable);
}
}
* @param integer $starttime Limit the calculation to this timestart
* @param integer $endtime Limit the calculation to this timeend
* @param array $existingcalculations Existing calculations of this indicator, indexed by sampleid.
- * @return array array[0] with format [sampleid] = int[]|float[], array[1] with format [sampleid] = int|float
+ * @return array [0] = [$sampleid => int[]|float[]], [1] = [$sampleid => int|float], [2] = [$sampleid => $sampleid]
*/
public function calculate($sampleids, $samplesorigin, $starttime = false, $endtime = false, $existingcalculations = array()) {
$calculations = array();
$newcalculations = array();
+ $notnulls = array();
foreach ($sampleids as $sampleid => $unusedsampleid) {
if (isset($existingcalculations[$sampleid])) {
$newcalculations[$sampleid] = $calculatedvalue;
}
- if (!is_null($calculatedvalue) && ($calculatedvalue > self::MAX_VALUE || $calculatedvalue < self::MIN_VALUE)) {
- throw new \coding_exception('Calculated values should be higher than ' . self::MIN_VALUE .
- ' and lower than ' . self::MAX_VALUE . ' ' . $calculatedvalue . ' received');
+ if (!is_null($calculatedvalue)) {
+ $notnulls[$sampleid] = $sampleid;
+ if ($calculatedvalue > self::MAX_VALUE || $calculatedvalue < self::MIN_VALUE) {
+ throw new \coding_exception('Calculated values should be higher than ' . self::MIN_VALUE .
+ ' and lower than ' . self::MAX_VALUE . ' ' . $calculatedvalue . ' received');
+ }
}
$calculations[$sampleid] = $calculatedvalue;
$features = $this->to_features($calculations);
- return array($features, $newcalculations);
+ return array($features, $newcalculations, $notnulls);
}
}
$range['start'], $range['end'], $samplesorigin);
}
+ // Here we store samples which calculations are not all null.
+ $notnulls = array();
+
// Fill the dataset samples with indicators data.
$newcalculations = array();
foreach ($indicators as $indicator) {
}
// Calculate the indicator for each sample in this time range.
- list($samplesfeatures, $newindicatorcalculations) = $rangeindicator->calculate($sampleids,
+ list($samplesfeatures, $newindicatorcalculations, $indicatornotnulls) = $rangeindicator->calculate($sampleids,
$samplesorigin, $range['start'], $range['end'], $prevcalculations);
// Copy the features data to the dataset.
$uniquesampleid = $this->append_rangeindex($analysersampleid, $rangeindex);
+ if (!isset($notnulls[$uniquesampleid]) && !empty($indicatornotnulls[$analysersampleid])) {
+ $notnulls[$uniquesampleid] = $uniquesampleid;
+ }
+
// Init the sample if it is still empty.
if (!isset($dataset[$uniquesampleid])) {
$dataset[$uniquesampleid] = array();
$DB->insert_records('analytics_indicator_calc', $newcalculations);
}
+ // Delete rows where all calculations are null.
+ // We still store the indicator calculation and we still store the sample id as
+ // processed so we don't have to process this sample again, but we exclude it
+ // from the dataset because it is not useful.
+ $nulls = array_diff_key($dataset, $notnulls);
+ foreach ($nulls as $uniqueid => $ignoredvalues) {
+ unset($dataset[$uniqueid]);
+ }
+
return $dataset;
}
}
}
+ /**
+ * Cleans up analytics db tables that do not directly depend on analysables that may have been deleted.
+ */
+ public static function cleanup() {
+ global $DB;
+
+ // Clean up stuff that depends on contexts that do not exist anymore.
+ $sql = "SELECT DISTINCT ap.contextid FROM {analytics_predictions} ap
+ LEFT JOIN {context} ctx ON ap.contextid = ctx.id
+ WHERE ctx.id IS NULL";
+ $apcontexts = $DB->get_records_sql($sql);
+
+ $sql = "SELECT DISTINCT aic.contextid FROM {analytics_indicator_calc} aic
+ LEFT JOIN {context} ctx ON aic.contextid = ctx.id
+ WHERE ctx.id IS NULL";
+ $indcalccontexts = $DB->get_records_sql($sql);
+
+ $contexts = $apcontexts + $indcalccontexts;
+ if ($contexts) {
+ list($sql, $params) = $DB->get_in_or_equal(array_keys($contexts));
+ $DB->execute("DELETE FROM {analytics_prediction_actions} WHERE predictionid IN
+ (SELECT ap.id FROM {analytics_predictions} ap WHERE ap.contextid $sql)", $params);
+
+ $DB->delete_records_select('analytics_predictions', "contextid $sql", $params);
+ $DB->delete_records_select('analytics_indicator_calc', "contextid $sql", $params);
+ }
+
+ // Clean up stuff that depends on analysable ids that do not exist anymore.
+ $models = self::get_all_models();
+ foreach ($models as $model) {
+ $analyser = $model->get_analyser(array('notimesplitting' => true));
+ $analysables = $analyser->get_analysables();
+ if (!$analysables) {
+ continue;
+ }
+
+ $analysableids = array_map(function($analysable) {
+ return $analysable->get_id();
+ }, $analysables);
+
+ list($notinsql, $params) = $DB->get_in_or_equal($analysableids, SQL_PARAMS_NAMED, 'param', false);
+ $params['modelid'] = $model->get_id();
+
+ $DB->delete_records_select('analytics_predict_samples', "modelid = :modelid AND analysableid $notinsql", $params);
+ $DB->delete_records_select('analytics_train_samples', "modelid = :modelid AND analysableid $notinsql", $params);
+ }
+ }
+
/**
* Returns the provided element classes in the site.
*
/**
* Returns the model analyser (defined by the model target).
*
+ * @param array $options Default initialisation with no options.
* @return \core_analytics\local\analyser\base
*/
- public function get_analyser() {
+ public function get_analyser($options = array()) {
if ($this->analyser !== null) {
return $this->analyser;
}
- // Default initialisation with no options.
- $this->init_analyser();
+ $this->init_analyser($options);
return $this->analyser;
}
throw new \moodle_exception('errornotarget', 'analytics');
}
- if (!empty($options['evaluation'])) {
- // The evaluation process will run using all available time splitting methods unless one is specified.
- if (!empty($options['timesplitting'])) {
- $timesplitting = \core_analytics\manager::get_time_splitting($options['timesplitting']);
- $timesplittings = array($timesplitting->get_id() => $timesplitting);
+ $timesplittings = array();
+ if (empty($options['notimesplitting'])) {
+ if (!empty($options['evaluation'])) {
+ // The evaluation process will run using all available time splitting methods unless one is specified.
+ if (!empty($options['timesplitting'])) {
+ $timesplitting = \core_analytics\manager::get_time_splitting($options['timesplitting']);
+ $timesplittings = array($timesplitting->get_id() => $timesplitting);
+ } else {
+ $timesplittings = \core_analytics\manager::get_enabled_time_splitting_methods();
+ }
} else {
- $timesplittings = \core_analytics\manager::get_enabled_time_splitting_methods();
- }
- } else {
- if (empty($this->model->timesplitting)) {
- throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
- }
+ if (empty($this->model->timesplitting)) {
+ throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
+ }
- // Returned as an array as all actions (evaluation, training and prediction) go through the same process.
- $timesplittings = array($this->model->timesplitting => $this->get_time_splitting());
- }
+ // Returned as an array as all actions (evaluation, training and prediction) go through the same process.
+ $timesplittings = array($this->model->timesplitting => $this->get_time_splitting());
+ }
- if (empty($timesplittings)) {
- throw new \moodle_exception('errornotimesplittings', 'analytics');
+ if (empty($timesplittings)) {
+ throw new \moodle_exception('errornotimesplittings', 'analytics');
+ }
}
if (!empty($options['evaluation'])) {
if ($this->model->timesplitting !== $timesplittingid ||
$this->model->indicators !== $indicatorsstr) {
- // We update the version of the model so different time splittings are not mixed up.
- $this->model->version = $now;
- // Delete generated predictions.
+ // Delete generated predictions before changing the model version.
$this->clear_model();
- // Purge all generated files.
- \core_analytics\dataset_manager::clear_model_files($this->model->id);
+ // It needs to be reset as the version changes.
+ $this->uniqueid = null;
+
+ // We update the version of the model so different time splittings are not mixed up.
+ $this->model->version = $now;
// Reset trained flag.
- $this->model->trained = 0;
+ if (!$this->is_static()) {
+ $this->model->trained = 0;
+ }
} else if ($this->model->enabled != $enabled) {
// We purge the cached contexts with insights as some will not be visible anymore.
$this->model->usermodified = $USER->id;
$DB->update_record('analytics_models', $this->model);
-
- // It needs to be reset (just in case, we may already used it).
- $this->uniqueid = null;
}
/**
\core_analytics\manager::check_can_manage_models();
$this->clear_model();
+
+ // Method self::clear_model is already clearing the current model version.
+ $predictor = \core_analytics\manager::get_predictions_processor();
+ $predictor->delete_output_dir($this->get_output_dir(array(), true));
+
$DB->delete_records('analytics_models', array('id' => $this->model->id));
+ $DB->delete_records('analytics_models_log', array('modelid' => $this->model->id));
}
/**
throw new \moodle_exception('errorinvalidtimesplitting', 'analytics');
}
+ // Delete generated predictions before changing the model version.
+ $this->clear_model();
+
+ // It needs to be reset as the version changes.
+ $this->uniqueid = null;
+
$this->model->timesplitting = $timesplittingid;
$this->model->version = $now;
+
+ // Reset trained flag.
+ if (!$this->is_static()) {
+ $this->model->trained = 0;
+ }
}
// Purge pages with insights as this may change things.
- if ($timesplittingid && $timesplittingid !== $this->model->timesplitting ||
- $this->model->enabled != 1) {
+ if ($this->model->enabled != 1) {
$this->purge_insights_cache();
}
// We don't always update timemodified intentionally as we reserve it for target, indicators or timesplitting updates.
$DB->update_record('analytics_models', $this->model);
-
- // It needs to be reset (just in case, we may already used it).
- $this->uniqueid = null;
}
/**
* models/$model->id/$model->version/execution
*
* @param array $subdirs
+ * @param bool $onlymodelid Preference over $subdirs
* @return string
*/
- protected function get_output_dir($subdirs = array()) {
+ protected function get_output_dir($subdirs = array(), $onlymodelid = false) {
global $CFG;
$subdirstr = '';
$outputdir = rtrim($CFG->dataroot, '/') . DIRECTORY_SEPARATOR . 'models';
}
- // Append model id and version + subdirs.
- $outputdir .= DIRECTORY_SEPARATOR . $this->model->id . DIRECTORY_SEPARATOR . $this->model->version . $subdirstr;
+ // Append model id
+ $outputdir .= DIRECTORY_SEPARATOR . $this->model->id;
+ if (!$onlymodelid) {
+ // Append version + subdirs.
+ $outputdir .= DIRECTORY_SEPARATOR . $this->model->version . $subdirstr;
+ }
make_writable_directory($outputdir);
private function clear_model() {
global $DB;
+ // Delete current model version stored stuff.
+ $predictor = \core_analytics\manager::get_predictions_processor();
+ $predictor->clear_model($this->get_unique_id(), $this->get_output_dir());
+
+ $predictionids = $DB->get_fieldset_select('analytics_predictions', 'id', 'modelid = :modelid',
+ array('modelid' => $this->get_id()));
+ if ($predictionids) {
+ list($sql, $params) = $DB->get_in_or_equal($predictionids);
+ $DB->delete_records_select('analytics_prediction_actions', "predictionid $sql", $params);
+ }
+
$DB->delete_records('analytics_predictions', array('modelid' => $this->model->id));
$DB->delete_records('analytics_predict_samples', array('modelid' => $this->model->id));
$DB->delete_records('analytics_train_samples', array('modelid' => $this->model->id));
$DB->delete_records('analytics_used_files', array('modelid' => $this->model->id));
+ // Purge all generated files.
+ \core_analytics\dataset_manager::clear_model_files($this->model->id);
+
// We don't expect people to clear models regularly and the cost of filling the cache is
// 1 db read per context.
$this->purge_insights_cache();
/**
* Constructor
*
- * @param \stdClass $prediction
+ * @param \stdClass|int $prediction
* @param array $sampledata
* @return void
*/
* @return bool
*/
public function is_ready();
+
+ /**
+ * Delete all stored information of the current model id.
+ *
+ * This method is called when there are important changes to a model,
+ * all previous training algorithms using that version of the model
+ * should be deleted.
+ *
+ * In case you want to perform extra security measures before deleting
+ * a directory you can check that $modelversionoutputdir subdirectories
+ * can only be named 'execution', 'evaluation' or 'testing'.
+ *
+ * @param string $uniqueid The site model unique id string
+ * @param string $modelversionoutputdir The output dir of this model version
+ * @return null
+ */
+ public function clear_model($uniqueid, $modelversionoutputdir);
+
+ /**
+ * Delete the output directory.
+ *
+ * This method is called when a model is completely deleted.
+ *
+ * In case you want to perform extra security measures before deleting
+ * a directory you can check that the subdirectories are timestamps
+ * (the model version) and each of this subdirectories' subdirectories
+ * can only be named 'execution', 'evaluation' or 'testing'.
+ *
+ * @param string $modeloutputdir The model directory id (parent of all model versions subdirectories).
+ * @return null
+ */
+ public function delete_output_dir($modeloutputdir);
+
}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Test target.
+ *
+ * @package core_analytics
+ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/test_target_shortname.php');
+
+/**
+ * Test target.
+ *
+ * @package core_analytics
+ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_target_course_level_shortname extends test_target_shortname {
+
+ /**
+ * get_analyser_class
+ *
+ * @return string
+ */
+ public function get_analyser_class() {
+ return '\core\analytics\analyser\courses';
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the manager.
+ *
+ * @package core_analytics
+ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/fixtures/test_indicator_max.php');
+require_once(__DIR__ . '/fixtures/test_indicator_min.php');
+require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
+require_once(__DIR__ . '/fixtures/test_target_course_level_shortname.php');
+
+/**
+ * Unit tests for the manager.
+ *
+ * @package core_analytics
+ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class analytics_manager_testcase extends advanced_testcase {
+
+ /**
+ * test_deleted_context
+ */
+ public function test_deleted_context() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+ $this->setAdminuser();
+ set_config('enabled_stores', 'logstore_standard', 'tool_log');
+
+ $target = \core_analytics\manager::get_target('test_target_course_level_shortname');
+ $indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname');
+ foreach ($indicators as $key => $indicator) {
+ $indicators[$key] = \core_analytics\manager::get_indicator($indicator);
+ }
+
+ $model = \core_analytics\model::create($target, $indicators);
+ $modelobj = $model->get_model_obj();
+
+ $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0));
+ $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0));
+ $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1));
+ $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1));
+
+ $model->enable('\core\analytics\time_splitting\no_splitting');
+
+ $model->train();
+ $model->predict();
+
+ // Generate a prediction action to confirm that it is deleted when there is an important update.
+ $predictions = $DB->get_records('analytics_predictions');
+ $prediction = reset($predictions);
+ $prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used'));
+ $prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $model->get_target());
+
+ $predictioncontextid = $prediction->get_prediction_data()->contextid;
+
+ $npredictions = $DB->count_records('analytics_predictions', array('contextid' => $predictioncontextid));
+ $npredictionactions = $DB->count_records('analytics_prediction_actions',
+ array('predictionid' => $prediction->get_prediction_data()->id));
+ $nindicatorcalc = $DB->count_records('analytics_indicator_calc', array('contextid' => $predictioncontextid));
+
+ \core_analytics\manager::cleanup();
+
+ // Nothing is incorrectly deleted.
+ $this->assertEquals($npredictions, $DB->count_records('analytics_predictions',
+ array('contextid' => $predictioncontextid)));
+ $this->assertEquals($npredictionactions, $DB->count_records('analytics_prediction_actions',
+ array('predictionid' => $prediction->get_prediction_data()->id)));
+ $this->assertEquals($nindicatorcalc, $DB->count_records('analytics_indicator_calc',
+ array('contextid' => $predictioncontextid)));
+
+ // Now we delete a context, the course predictions and prediction actions should be deleted.
+ $deletedcontext = \context::instance_by_id($predictioncontextid);
+ delete_course($deletedcontext->instanceid, false);
+
+ \core_analytics\manager::cleanup();
+
+ $this->assertEmpty($DB->count_records('analytics_predictions', array('contextid' => $predictioncontextid)));
+ $this->assertEmpty($DB->count_records('analytics_prediction_actions',
+ array('predictionid' => $prediction->get_prediction_data()->id)));
+ $this->assertEmpty($DB->count_records('analytics_indicator_calc', array('contextid' => $predictioncontextid)));
+
+ set_config('enabled_stores', '', 'tool_log');
+ get_log_manager(true);
+ }
+
+ /**
+ * test_deleted_analysable
+ */
+ public function test_deleted_analysable() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+ $this->setAdminuser();
+ set_config('enabled_stores', 'logstore_standard', 'tool_log');
+
+ $target = \core_analytics\manager::get_target('test_target_course_level_shortname');
+ $indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname');
+ foreach ($indicators as $key => $indicator) {
+ $indicators[$key] = \core_analytics\manager::get_indicator($indicator);
+ }
+
+ $model = \core_analytics\model::create($target, $indicators);
+ $modelobj = $model->get_model_obj();
+
+ $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0));
+ $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0));
+ $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1));
+ $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1));
+
+ $model->enable('\core\analytics\time_splitting\no_splitting');
+
+ $model->train();
+ $model->predict();
+
+ $npredictsamples = $DB->count_records('analytics_predict_samples');
+ $ntrainsamples = $DB->count_records('analytics_train_samples');
+
+ // Now we delete an analysable, stored predict and training samples should be deleted.
+ $deletedcontext = \context_course::instance($coursepredict1->id);
+ delete_course($coursepredict1, false);
+
+ \core_analytics\manager::cleanup();
+
+ $this->assertEmpty($DB->count_records('analytics_predict_samples', array('analysableid' => $coursepredict1->id)));
+ $this->assertEmpty($DB->count_records('analytics_train_samples', array('analysableid' => $coursepredict1->id)));
+
+ set_config('enabled_stores', '', 'tool_log');
+ get_log_manager(true);
+ }
+
+}
$this->assertInstanceOf('\core_analytics\model', $model);
}
+ /**
+ * test_delete
+ */
+ public function test_delete() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+ set_config('enabled_stores', 'logstore_standard', 'tool_log');
+
+ $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0));
+ $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0));
+ $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1));
+ $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1));
+
+ $this->model->enable('\core\analytics\time_splitting\no_splitting');
+
+ $this->model->train();
+ $this->model->predict();
+
+ // Fake evaluation results record to check that it is actually deleted.
+ $this->add_fake_log();
+
+ $modeloutputdir = $this->model->get_output_dir(array(), true);
+ $this->assertTrue(is_dir($modeloutputdir));
+
+ // Generate a prediction action to confirm that it is deleted when there is an important update.
+ $predictions = $DB->get_records('analytics_predictions');
+ $prediction = reset($predictions);
+ $prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used'));
+ $prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
+
+ $this->model->delete();
+ $this->assertEmpty($DB->count_records('analytics_models', array('id' => $this->modelobj->id)));
+ $this->assertEmpty($DB->count_records('analytics_models_log', array('modelid' => $this->modelobj->id)));
+ $this->assertEmpty($DB->count_records('analytics_predictions'));
+ $this->assertEmpty($DB->count_records('analytics_prediction_actions'));
+ $this->assertEmpty($DB->count_records('analytics_train_samples'));
+ $this->assertEmpty($DB->count_records('analytics_predict_samples'));
+ $this->assertEmpty($DB->count_records('analytics_used_files'));
+ $this->assertFalse(is_dir($modeloutputdir));
+
+ set_config('enabled_stores', '', 'tool_log');
+ get_log_manager(true);
+ }
+
+ /**
+ * test_clear
+ */
+ public function test_clear() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+ set_config('enabled_stores', 'logstore_standard', 'tool_log');
+
+ $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0));
+ $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0));
+ $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1));
+ $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1));
+
+ $this->model->enable('\core\analytics\time_splitting\no_splitting');
+
+ $this->model->train();
+ $this->model->predict();
+
+ // Fake evaluation results record to check that it is actually deleted.
+ $this->add_fake_log();
+
+ // Generate a prediction action to confirm that it is deleted when there is an important update.
+ $predictions = $DB->get_records('analytics_predictions');
+ $prediction = reset($predictions);
+ $prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used'));
+ $prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
+
+ $modelversionoutputdir = $this->model->get_output_dir();
+ $this->assertTrue(is_dir($modelversionoutputdir));
+
+ // Update to an empty time splitting method to force clear_model execution.
+ $this->model->update(1, false, '');
+ $this->assertFalse(is_dir($modelversionoutputdir));
+
+ // Restore previous time splitting method.
+ $this->model->enable('\core\analytics\time_splitting\no_splitting');
+
+ // Check that most of the stuff got deleted.
+ $this->assertEquals(1, $DB->count_records('analytics_models', array('id' => $this->modelobj->id)));
+ $this->assertEquals(1, $DB->count_records('analytics_models_log', array('modelid' => $this->modelobj->id)));
+ $this->assertEmpty($DB->count_records('analytics_predictions'));
+ $this->assertEmpty($DB->count_records('analytics_prediction_actions'));
+ $this->assertEmpty($DB->count_records('analytics_train_samples'));
+ $this->assertEmpty($DB->count_records('analytics_predict_samples'));
+ $this->assertEmpty($DB->count_records('analytics_used_files'));
+
+ set_config('enabled_stores', '', 'tool_log');
+ get_log_manager(true);
+ }
+
public function test_model_manager() {
$this->resetAfterTest(true);
$modeldir = $dir . DIRECTORY_SEPARATOR . $this->modelobj->id . DIRECTORY_SEPARATOR . $this->modelobj->version;
$this->assertEquals($modeldir, $this->model->get_output_dir());
- $this->assertEquals($modeldir . DIRECTORY_SEPARATOR . 'asd', $this->model->get_output_dir(array('asd')));
+ $this->assertEquals($modeldir . DIRECTORY_SEPARATOR . 'testing', $this->model->get_output_dir(array('testing')));
}
public function test_unique_id() {
$target = \core_analytics\manager::get_target('\core\analytics\target\no_teaching');
$this->assertTrue(\core_analytics\model::exists($target));
}
+
+ /**
+ * Generates a model log record.
+ */
+ private function add_fake_log() {
+ global $DB, $USER;
+
+ $log = new stdClass();
+ $log->modelid = $this->modelobj->id;
+ $log->version = $this->modelobj->version;
+ $log->target = $this->modelobj->target;
+ $log->indicators = $this->modelobj->indicators;
+ $log->score = 1;
+ $log->info = json_encode([]);
+ $log->dir = 'not important';
+ $log->timecreated = time();
+ $log->usermodified = $USER->id;
+ $DB->insert_record('analytics_models_log', $log);
+ }
}
/**
* get_output_dir
*
* @param array $subdirs
+ * @param bool $onlymodelid
* @return string
*/
- public function get_output_dir($subdirs = array()) {
- return parent::get_output_dir($subdirs);
+ public function get_output_dir($subdirs = array(), $onlymodelid = false) {
+ return parent::get_output_dir($subdirs, $onlymodelid);
}
/**
list($values, $unused) = $indicator->calculate($sampleids, $sampleorigin, $starttime, $endtime, $existingcalcs);
}
+ /**
+ * test_not_null_samples
+ */
+ public function test_not_null_samples() {
+ $this->resetAfterTest(true);
+
+ $classname = '\core\analytics\time_splitting\quarters';
+ $timesplitting = \core_analytics\manager::get_time_splitting($classname);
+ $timesplitting->set_analysable(new \core_analytics\site());
+
+ $ranges = array(
+ array('start' => 111, 'end' => 222, 'time' => 222),
+ array('start' => 222, 'end' => 333, 'time' => 333)
+ );
+ $samples = array(123 => 123, 321 => 321);
+
+ $indicator1 = $this->getMockBuilder('test_indicator_max')
+ ->setMethods(['calculate_sample'])
+ ->getMock();
+ $indicator1->method('calculate_sample')
+ ->willReturn(null);
+
+ $indicator2 = \core_analytics\manager::get_indicator('test_indicator_min');
+
+ // Samples with at least 1 not null value are returned.
+ $params = array(
+ $samples,
+ 'whatever',
+ array($indicator1, $indicator2),
+ $ranges
+ );
+ $dataset = phpunit_util::call_internal_method($timesplitting, 'calculate_indicators', $params, $classname);
+ $this->assertArrayHasKey('123-0', $dataset);
+ $this->assertArrayHasKey('123-1', $dataset);
+ $this->assertArrayHasKey('321-0', $dataset);
+ $this->assertArrayHasKey('321-1', $dataset);
+
+ // Samples with only null values are not returned.
+ $params = array(
+ $samples,
+ 'whatever',
+ array($indicator1),
+ $ranges
+ );
+ $dataset = phpunit_util::call_internal_method($timesplitting, 'calculate_indicators', $params, $classname);
+ $this->assertArrayNotHasKey('123-0', $dataset);
+ $this->assertArrayNotHasKey('123-1', $dataset);
+ $this->assertArrayNotHasKey('321-0', $dataset);
+ $this->assertArrayNotHasKey('321-1', $dataset);
+ }
+
/**
* provider_ml_test_evaluation
*
public function get_content() {
global $CFG;
- $calm = optional_param('cal_m', 0, PARAM_INT);
- $caly = optional_param('cal_y', 0, PARAM_INT);
- $time = optional_param('time', 0, PARAM_INT);
-
require_once($CFG->dirroot.'/calendar/lib.php');
if ($this->content !== null) {
return $this->content;
}
- // If a day, month and year were passed then convert it to a timestamp. If these were passed then we can assume
- // the day, month and year are passed as Gregorian, as no where in core should we be passing these values rather
- // than the time. This is done for BC.
- if (!empty($calm) && (!empty($caly))) {
- $time = make_timestamp($caly, $calm, 1);
- } else if (empty($time)) {
- $time = time();
- }
-
$this->content = new stdClass;
$this->content->text = '';
$this->content->footer = '';
- // [pj] To me it looks like this if would never be needed, but Penny added it
- // when committing the /my/ stuff. Reminder to discuss and learn what it's about.
- // It definitely needs SOME comment here!
$courseid = $this->page->course->id;
$issite = ($courseid == SITEID);
+ $course = null;
+ $courses = null;
+ $categories = null;
+
if ($issite) {
// Being displayed at site level. This will cause the filter to fall back to auto-detecting
// the list of courses it will be grabbing events from.
$course = get_site();
$courses = calendar_get_default_courses();
+
+ if ($this->page->context->contextlevel === CONTEXT_COURSECAT) {
+ // Restrict to categories, and their parents, and the courses that the user is enrolled in within those
+ // categories.
+ $categories = array_keys($this->page->categories);
+ $courses = array_filter($courses, function($course) use ($categories) {
+ return array_search($course->category, $categories) !== false;
+ });
+ }
} else {
// Forcibly filter events to include only those from the particular course we are in.
$course = $this->page->course;
$renderer = $this->page->get_renderer('core_calendar');
- $calendar = new calendar_information(0, 0, 0, $time);
- $calendar->prepare_for_view($course, $courses);
+ $calendar = new calendar_information();
+ $calendar->set_sources($course, $courses, $this->page->category);
list($data, $template) = calendar_get_view($calendar, 'mini');
$this->content->text .= $renderer->render_from_template($template, $data);
'core_calendar/summary_modal',
'core_calendar/repository',
'core_calendar/events',
- 'core_calendar/view_manager'
+ 'core_calendar/view_manager',
+ 'core_calendar/crud',
+ 'core_calendar/selectors',
],
function(
$,
SummaryModal,
CalendarRepository,
CalendarEvents,
- CalendarViewManager
+ CalendarViewManager,
+ CalendarCrud,
+ CalendarSelectors
) {
var SELECTORS = {
}
};
- /**
- * Create the event form modal for creating new events and
- * editing existing events.
- *
- * @method registerEventFormModal
- * @param {object} root The calendar root element
- * @return {object} The create modal promise
- */
- var registerEventFormModal = function(root) {
- var newEventButton = root.find(SELECTORS.NEW_EVENT_BUTTON);
- var contextId = newEventButton.attr('data-context-id');
-
- return ModalFactory.create(
- {
- type: ModalEventForm.TYPE,
- large: true,
- templateContext: {
- contextid: contextId
- }
- }
- );
- };
-
/**
* Listen to and handle any calendar events fired by the calendar UI.
*
* @param {object} eventFormModalPromise A promise reolved with the event form modal
*/
var registerCalendarEventListeners = function(root, eventFormModalPromise) {
- var body = $('body'),
- courseId = $(root).find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
+ var body = $('body');
body.on(CalendarEvents.created, function() {
CalendarViewManager.reloadCurrentMonth(root);
CalendarViewManager.reloadCurrentMonth(root);
});
- eventFormModalPromise.then(function(modal) {
+ eventFormModalPromise
+ .then(function(modal) {
// When something within the calendar tells us the user wants
// to edit an event then show the event form modal.
body.on(CalendarEvents.editEvent, function(e, eventId) {
+ var calendarWrapper = root.find(CalendarSelectors.wrapper);
modal.setEventId(eventId);
+ modal.setContextId(calendarWrapper.data('contextId'));
modal.show();
});
- modal.setCourseId(courseId);
return;
})
.fail(Notification.exception);
root.on('change', SELECTORS.COURSE_SELECTOR, function() {
var selectElement = $(this);
var courseId = selectElement.val();
- CalendarViewManager.reloadCurrentMonth(root, courseId)
+ CalendarViewManager.reloadCurrentMonth(root, courseId, null)
.then(function() {
// We need to get the selector again because the content has changed.
return root.find(SELECTORS.COURSE_SELECTOR).val(courseId);
.fail(Notification.exception);
});
- var eventFormPromise = registerEventFormModal(root);
+ var eventFormPromise = CalendarCrud.registerEventFormModal(root);
registerCalendarEventListeners(root, eventFormPromise);
- // Bind click event on the new event button.
- root.on('click', SELECTORS.NEW_EVENT_BUTTON, function(e) {
- eventFormPromise.then(function(modal) {
- // Attempt to find the cell for today.
- // If it can't be found, then use the start time of the first day on the calendar.
- var today = root.find(SELECTORS.TODAY);
- if (!today.length) {
- modal.setStartTime(root.find(SELECTORS.DAY).attr('data-new-event-timestamp'));
- }
-
- modal.show();
- return;
- })
- .fail(Notification.exception);
-
- e.preventDefault();
- });
-
// Bind click events to calendar days.
root.on('click', SELECTORS.DAY, function(e) {
+
var target = $(e.target);
if (!target.is(SELECTORS.VIEW_DAY_LINK)) {
var startTime = $(this).attr('data-new-event-timestamp');
eventFormPromise.then(function(modal) {
+ var wrapper = target.closest(CalendarSelectors.wrapper);
+ modal.setCourseId(wrapper.data('courseid'));
+
+ var categoryId = wrapper.data('categoryid');
+ if (typeof categoryId !== 'undefined') {
+ modal.setCategoryId(categoryId);
+ }
+
+ modal.setContextId(wrapper.data('contextId'));
modal.setStartTime(startTime);
modal.show();
return;
return {
init: function(root) {
root = $(root);
-
CalendarViewManager.init(root);
registerEventListeners(root);
}
*/
define([
'jquery',
+ 'core/notification',
'core_calendar/selectors',
'core_calendar/events',
'core/templates',
],
function(
$,
+ Notification,
CalendarSelectors,
CalendarEvents,
Templates,
*/
var registerCalendarEventListeners = function(root) {
var body = $('body');
- body.on(CalendarEvents.monthChanged, function(e, year, month, courseId) {
+ body.on(CalendarEvents.monthChanged, function(e, year, month, courseId, categoryId) {
// We have to use a queue here because the calling code is decoupled from these listeners.
// It's possible for the event to be called multiple times before one call is fully resolved.
root.queue(function(next) {
- return processRequest(e, year, month, courseId)
+ return processRequest(e, year, month, courseId, categoryId)
.then(function() {
return next();
- });
+ })
+ .fail(Notification.exception)
+ ;
});
});
- var processRequest = function(e, year, month, courseId) {
+ var processRequest = function(e, year, month, courseId, categoryId) {
var newCurrentMonth = root.find('[data-year="' + year + '"][data-month="' + month + '"]');
var newParent = newCurrentMonth.closest(CalendarSelectors.calendarPeriods.month);
var allMonths = root.find(CalendarSelectors.calendarPeriods.month);
requestYear,
requestMonth,
courseId,
+ categoryId,
placeHolder
)
.then(function() {
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This module is responsible for handle calendar day and upcoming view.
+ *
+ * @module core_calendar/calendar
+ * @package core_calendar
+ * @copyright 2017 Simey Lameze <simey@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+ 'jquery',
+ 'core/str',
+ 'core/notification',
+ 'core_calendar/selectors',
+ 'core_calendar/events',
+ 'core_calendar/view_manager',
+ 'core_calendar/repository',
+ 'core/modal_factory',
+ 'core_calendar/modal_event_form',
+ 'core/modal_events',
+ 'core_calendar/crud'
+ ],
+ function(
+ $,
+ Str,
+ Notification,
+ CalendarSelectors,
+ CalendarEvents,
+ CalendarViewManager,
+ CalendarRepository,
+ ModalFactory,
+ ModalEventForm,
+ ModalEvents,
+ CalendarCrud
+ ) {
+
+ var registerEventListeners = function(root, type) {
+ var body = $('body');
+
+ CalendarCrud.registerEventFormModal(root);
+ CalendarCrud.registerRemove(root);
+
+ var reloadFunction = 'reloadCurrent' + type.charAt(0).toUpperCase() + type.slice(1);
+
+ body.on(CalendarEvents.created, function() {
+ CalendarViewManager[reloadFunction](root);
+ });
+ body.on(CalendarEvents.deleted, function() {
+ CalendarViewManager[reloadFunction](root);
+ });
+ body.on(CalendarEvents.updated, function() {
+ CalendarViewManager[reloadFunction](root);
+ });
+
+ root.on('change', CalendarSelectors.courseSelector, function() {
+ var selectElement = $(this);
+ var courseId = selectElement.val();
+ CalendarViewManager[reloadFunction](root, courseId)
+ .then(function() {
+ // We need to get the selector again because the content has changed.
+ return root.find(CalendarSelectors.courseSelector).val(courseId);
+ })
+ .fail(Notification.exception);
+ });
+
+ body.on(CalendarEvents.filterChanged, function(e, data) {
+ var daysWithEvent = root.find(CalendarSelectors.eventType[data.type]);
+ if (data.hidden == true) {
+ daysWithEvent.addClass('hidden');
+ } else {
+ daysWithEvent.removeClass('hidden');
+ }
+ });
+ };
+
+ return {
+ init: function(root, type) {
+ root = $(root);
+
+ CalendarViewManager.init(root, type);
+ registerEventListeners(root, type);
+ }
+ };
+ });
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A module to handle CRUD operations within the UI.
+ *
+ * @module core_calendar/crud
+ * @package core_calendar
+ * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+ 'jquery',
+ 'core/str',
+ 'core/notification',
+ 'core/custom_interaction_events',
+ 'core/modal',
+ 'core/modal_registry',
+ 'core/modal_factory',
+ 'core/modal_events',
+ 'core_calendar/modal_event_form',
+ 'core_calendar/repository',
+ 'core_calendar/events',
+ 'core_calendar/modal_delete',
+ 'core_calendar/selectors',
+],
+function(
+ $,
+ Str,
+ Notification,
+ CustomEvents,
+ Modal,
+ ModalRegistry,
+ ModalFactory,
+ ModalEvents,
+ ModalEventForm,
+ CalendarRepository,
+ CalendarEvents,
+ ModalDelete,
+ CalendarSelectors
+) {
+
+ /**
+ * Prepares the action for the summary modal's delete action.
+ *
+ * @param {Number} eventId The ID of the event.
+ * @param {string} eventTitle The event title.
+ * @param {Number} eventCount The number of events in the series.
+ * @return {Promise}
+ */
+ function confirmDeletion(eventId, eventTitle, eventCount) {
+ var deleteStrings = [
+ {
+ key: 'deleteevent',
+ component: 'calendar'
+ },
+ ];
+
+ eventCount = parseInt(eventCount, 10);
+ var deletePromise;
+ var isRepeatedEvent = eventCount > 1;
+ if (isRepeatedEvent) {
+ deleteStrings.push({
+ key: 'confirmeventseriesdelete',
+ component: 'calendar',
+ param: {
+ name: eventTitle,
+ count: eventCount,
+ },
+ });
+
+ deletePromise = ModalFactory.create(
+ {
+ type: ModalDelete.TYPE
+ }
+ );
+ } else {
+ deleteStrings.push({
+ key: 'confirmeventdelete',
+ component: 'calendar',
+ param: eventTitle
+ });
+
+
+ deletePromise = ModalFactory.create(
+ {
+ type: ModalFactory.types.SAVE_CANCEL
+ }
+ );
+ }
+
+ deletePromise.then(function(deleteModal) {
+ deleteModal.show();
+
+ return;
+ })
+ .fail(Notification.exception);
+
+ var stringsPromise = Str.get_strings(deleteStrings);
+
+ var finalPromise = $.when(stringsPromise, deletePromise)
+ .then(function(strings, deleteModal) {
+ deleteModal.setTitle(strings[0]);
+ deleteModal.setBody(strings[1]);
+ if (!isRepeatedEvent) {
+ deleteModal.setSaveButtonText(strings[0]);
+ }
+
+ deleteModal.getRoot().on(ModalEvents.save, function() {
+ CalendarRepository.deleteEvent(eventId, false)
+ .then(function() {
+ $('body').trigger(CalendarEvents.deleted, [eventId, false]);
+ return;
+ })
+ .catch(Notification.exception);
+ });
+
+ deleteModal.getRoot().on(CalendarEvents.deleteAll, function() {
+ CalendarRepository.deleteEvent(eventId, true)
+ .then(function() {
+ $('body').trigger(CalendarEvents.deleted, [eventId, true]);
+ return;
+ })
+ .catch(Notification.exception);
+ });
+
+ return deleteModal;
+ })
+ .fail(Notification.exception);
+
+ return finalPromise;
+ }
+
+ /**
+ * Create the event form modal for creating new events and
+ * editing existing events.
+ *
+ * @method registerEventFormModal
+ * @param {object} root The calendar root element
+ * @return {object} The create modal promise
+ */
+ var registerEventFormModal = function(root) {
+ var eventFormPromise = ModalFactory.create({
+ type: ModalEventForm.TYPE,
+ large: true
+ });
+
+ // Bind click event on the new event button.
+ root.on('click', CalendarSelectors.actions.create, function(e) {
+ eventFormPromise.then(function(modal) {
+ var wrapper = root.find(CalendarSelectors.wrapper);
+
+ var categoryId = wrapper.data('categoryid');
+ if (typeof categoryId !== 'undefined') {
+ modal.setCategoryId(categoryId);
+ }
+
+ // Attempt to find the cell for today.
+ // If it can't be found, then use the start time of the first day on the calendar.
+ var today = root.find(CalendarSelectors.today);
+ var firstDay = root.find(CalendarSelectors.day);
+ if (!today.length && firstDay.length) {
+ modal.setStartTime(firstDay.data('newEventTimestamp'));
+ }
+
+ modal.setContextId(wrapper.data('contextId'));
+ modal.setCourseId(wrapper.data('courseid'));
+ modal.show();
+ return;
+ })
+ .fail(Notification.exception);
+
+ e.preventDefault();
+ });
+
+ root.on('click', CalendarSelectors.actions.edit, function(e) {
+ e.preventDefault();
+ var target = $(e.currentTarget),
+ calendarWrapper = target.closest(CalendarSelectors.wrapper),
+ eventWrapper = target.closest(CalendarSelectors.eventItem);
+
+ eventFormPromise.then(function(modal) {
+ // When something within the calendar tells us the user wants
+ // to edit an event then show the event form modal.
+ modal.setEventId(eventWrapper.data('eventId'));
+
+ modal.setContextId(calendarWrapper.data('contextId'));
+ modal.show();
+ return;
+ }).fail(Notification.exception);
+ });
+
+
+ return eventFormPromise;
+ };
+ /**
+ * Register the listeners required to remove the event.
+ *
+ * @param {jQuery} root
+ */
+ function registerRemove(root) {
+ root.on('click', CalendarSelectors.actions.remove, function(e) {
+ // Fetch the event title, count, and pass them into the new dialogue.
+ var eventSource = $(this).closest(CalendarSelectors.eventItem);
+ var eventId = eventSource.data('eventId'),
+ eventTitle = eventSource.data('eventTitle'),
+ eventCount = eventSource.data('eventCount');
+ confirmDeletion(eventId, eventTitle, eventCount);
+
+ e.preventDefault();
+ });
+ }
+
+ return {
+ registerRemove: registerRemove,
+ registerEventFormModal: registerEventFormModal
+ };
+});
editEvent: 'calendar-events:edit_event',
editActionEvent: 'calendar-events:edit_action_event',
eventMoved: 'calendar-events:event_moved',
+ dayChanged: 'calendar-events:day_changed',
monthChanged: 'calendar-events:month_changed',
moveEvent: 'calendar-events:move_event',
filterChanged: 'calendar-events:filter_changed',
this.eventId = null;
this.startTime = null;
this.courseId = null;
+ this.categoryId = null;
+ this.contextId = null;
this.reloadingBody = false;
this.reloadingTitle = false;
this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
ModalEventForm.prototype = Object.create(Modal.prototype);
ModalEventForm.prototype.constructor = ModalEventForm;
+ /**
+ * Set the context id to the given value.
+ *
+ * @method setContextId
+ * @param {Number} id The event id
+ */
+ ModalEventForm.prototype.setContextId = function(id) {
+ this.contextId = id;
+ };
+
+ /**
+ * Retrieve the current context id, if any.
+ *
+ * @method getContextId
+ * @return {Number|null} The event id
+ */
+ ModalEventForm.prototype.getContextId = function() {
+ return this.contextId;
+ };
+
/**
* Set the course id to the given value.
*
return this.courseId;
};
+ /**
+ * Set the category id to the given value.
+ *
+ * @method setCategoryId
+ * @param {int} id The event id
+ */
+ ModalEventForm.prototype.setCategoryId = function(id) {
+ this.categoryId = id;
+ };
+
+ /**
+ * Retrieve the current category id, if any.
+ *
+ * @method getCategoryId
+ * @return {int|null} The event id
+ */
+ ModalEventForm.prototype.getCategoryId = function() {
+ return this.categoryId;
+ };
+
/**
* Check if the modal has an course id.
*
return this.courseId !== null;
};
+ /**
+ * Check if the modal has an category id.
+ *
+ * @method hasCategoryId
+ * @return {bool}
+ */
+ ModalEventForm.prototype.hasCategoryId = function() {
+ return this.categoryId !== null;
+ };
+
/**
* Set the event id to the given value.
*
.always(function() {
this.reloadingTitle = false;
return;
- }.bind(this));
+ }.bind(this))
+ .fail(Notification.exception);
return this.titlePromise;
};
this.reloadingBody = true;
this.disableButtons();
- var contextId = this.saveButton.attr('data-context-id');
var args = {};
if (this.hasEventId()) {
args.courseid = this.getCourseId();
}
+ if (this.hasCategoryId()) {
+ args.categoryid = this.getCategoryId();
+ }
+
if (typeof formData !== 'undefined') {
args.formdata = formData;
}
- this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', contextId, args);
+ this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', this.getContextId(), args);
this.setBody(this.bodyPromise);
this.enableButtons();
return;
}.bind(this))
- .catch(Notification.exception)
+ .fail(Notification.exception)
.always(function() {
this.reloadingBody = false;
return;
- }.bind(this));
+ }.bind(this))
+ .fail(Notification.exception);
return this.bodyPromise;
};
Modal.prototype.hide.call(this);
this.setEventId(null);
this.setStartTime(null);
+ this.setCourseId(null);
+ this.setCategoryId(null);
};
/**
// If there was a server side validation error then
// we need to re-request the rendered form from the server
// in order to display the error for the user.
- return this.reloadBodyContent(formData);
+ this.reloadBodyContent(formData);
+ return;
} else {
// No problemo! Our work here is done.
this.hide();
// the loading icon and re-enable the buttons.
loadingContainer.addClass('hidden');
this.enableButtons();
+
+ return;
}.bind(this))
- .catch(Notification.exception);
+ .fail(Notification.exception);
};
/**
*
* @method deleteEvent
* @param {int} eventId The event id.
- * @arapm {bool} deleteSeries Whether to delete all events in the series
+ * @param {bool} deleteSeries Whether to delete all events in the series
* @return {promise} Resolved with requested calendar event
*/
var deleteEvent = function(eventId, deleteSeries) {
* @param {Number} year Year
* @param {Number} month Month
* @param {Number} courseid The course id.
+ * @param {Number} categoryid The category id.
* @param {Bool} includenavigation Whether to include navigation.
* @return {promise} Resolved with the month view data.
*/
- var getCalendarMonthData = function(year, month, courseid, includenavigation) {
+ var getCalendarMonthData = function(year, month, courseid, categoryid, includenavigation) {
var request = {
methodname: 'core_calendar_get_calendar_monthly_view',
args: {
year: year,
month: month,
courseid: courseid,
+ categoryid: categoryid,
includenavigation: includenavigation,
}
};
return Ajax.call([request])[0];
};
+ /**
+ * Get calendar data for the day view.
+ *
+ * @method getCalendarDayData
+ * @param {Number} year Year
+ * @param {Number} month Month
+ * @param {Number} day Day
+ * @param {Number} courseid The course id.
+ * @param {Number} categoryId The id of the category whose events are shown
+ * @return {promise} Resolved with the day view data.
+ */
+ var getCalendarDayData = function(year, month, day, courseid, categoryId) {
+ var request = {
+ methodname: 'core_calendar_get_calendar_day_view',
+ args: {
+ year: year,
+ month: month,
+ day: day,
+ courseid: courseid,
+ categoryid: categoryId,
+ }
+ };
+
+ return Ajax.call([request])[0];
+ };
+
/**
* Change the start day for the given event id. The day timestamp
* only has to be any time during the target day because only the
return Ajax.call([request])[0];
};
+ /**
+ * Get calendar upcoming data.
+ *
+ * @method getCalendarUpcomingData
+ * @param {Number} courseid The course id.
+ * @return {promise} Resolved with the month view data.
+ */
+ var getCalendarUpcomingData = function(courseid) {
+ var request = {
+ methodname: 'core_calendar_get_calendar_upcoming_view',
+ args: {
+ courseid: courseid,
+ }
+ };
+
+ return Ajax.call([request])[0];
+ };
+
return {
getEventById: getEventById,
deleteEvent: deleteEvent,
updateEventStartDay: updateEventStartDay,
submitCreateUpdateForm: submitCreateUpdateForm,
- getCalendarMonthData: getCalendarMonthData
+ getCalendarMonthData: getCalendarMonthData,
+ getCalendarDayData: getCalendarDayData,
+ getCalendarUpcomingData: getCalendarUpcomingData
};
});
eventFilterItem: "[data-action='filter-event-type']",
eventType: {
site: "[data-eventtype-site]",
+ category: "[data-eventtype-category]",
course: "[data-eventtype-course]",
group: "[data-eventtype-group]",
user: "[data-eventtype-user]",
},
popoverType: {
site: "[data-popover-eventtype-site]",
+ category: "[data-popover-eventtype-category]",
course: "[data-popover-eventtype-course]",
group: "[data-popover-eventtype-group]",
user: "[data-popover-eventtype-user]",
calendarPeriods: {
month: "[data-period='month']",
},
+ courseSelector: 'select[name="course"]',
+ actions: {
+ create: '[data-action="new-event-button"]',
+ edit: '[data-action="edit"]',
+ remove: '[data-action="delete"]',
+ },
+ today: '.today',
+ day: '[data-region="day"]',
+ wrapper: '.calendarwrapper',
+ eventItem: '[data-type="event"]',
};
});
'core/modal_events',
'core_calendar/repository',
'core_calendar/events',
- 'core_calendar/modal_delete',
+ 'core_calendar/crud',
],
function(
$,
ModalEvents,
CalendarRepository,
CalendarEvents,
- ModalDelete
+ CalendarCrud
) {
var registered = false;
return this.getBody().find(SELECTORS.ROOT).attr('data-event-id');
};
+ /**
+ * Get the title for the event being shown in this modal. This value is
+ * not cached because it will change depending on which event is
+ * being displayed.
+ *
+ * @method getEventTitle
+ * @return {String}
+ */
+ ModalEventSummary.prototype.getEventTitle = function() {
+ return this.getBody().find(SELECTORS.ROOT).attr('data-event-title');
+ };
+
/**
* Get the number of events in the series for the event being shown in
* this modal. This value is not cached because it will change
* @return {int}
*/
ModalEventSummary.prototype.getEventCount = function() {
- return this.getBody().find(SELECTORS.ROOT).attr('data-event-event-count');
+ return this.getBody().find(SELECTORS.ROOT).attr('data-event-count');
};
/**
// We have to wait for the modal to finish rendering in order to ensure that
// the data-event-title property is available to use as the modal title.
this.getRoot().on(ModalEvents.bodyRendered, function() {
- var eventTitle = this.getBody().find(SELECTORS.ROOT).attr('data-event-title');
- prepareDeleteAction(this, eventTitle);
+ this.getModal().data({
+ eventTitle: this.getEventTitle(),
+ eventId: this.getEventId(),
+ eventCount: this.getEventCount(),
+ })
+ .attr('data-type', 'event');
+ CalendarCrud.registerRemove(this.getModal());
+
+ }.bind(this));
+
+ $('body').on(CalendarEvents.deleted, function() {
+ // Close the dialogue on delete.
+ this.hide();
}.bind(this));
CustomEvents.define(this.getEditButton(), [
]);
this.getEditButton().on(CustomEvents.events.activate, function(e, data) {
-
if (this.isActionEvent()) {
// Action events cannot be edited on the event form and must be redirected to the module UI.
$('body').trigger(CalendarEvents.editActionEvent, [this.getEditUrl()]);
}.bind(this));
};
- /**
- * Prepares the action for the summary modal's delete action.
- *
- * @param {ModalEventSummary} summaryModal The summary modal instance.
- * @param {string} eventTitle The event title.
- */
- function prepareDeleteAction(summaryModal, eventTitle) {
- var deleteStrings = [
- {
- key: 'deleteevent',
- component: 'calendar'
- },
- ];
-
- var eventCount = parseInt(summaryModal.getEventCount(), 10);
- var deletePromise;
- var isRepeatedEvent = eventCount > 1;
- if (isRepeatedEvent) {
- deleteStrings.push({
- key: 'confirmeventseriesdelete',
- component: 'calendar',
- param: {
- name: eventTitle,
- count: eventCount,
- },
- });
-
- deletePromise = ModalFactory.create(
- {
- type: ModalDelete.TYPE
- },
- summaryModal.getDeleteButton()
- );
- } else {
- deleteStrings.push({
- key: 'confirmeventdelete',
- component: 'calendar',
- param: eventTitle
- });
-
- deletePromise = ModalFactory.create(
- {
- type: ModalFactory.types.SAVE_CANCEL
- },
- summaryModal.getDeleteButton()
- );
- }
-
- var eventId = summaryModal.getEventId();
- var stringsPromise = Str.get_strings(deleteStrings);
-
- $.when(stringsPromise, deletePromise)
- .then(function(strings, deleteModal) {
- deleteModal.setTitle(strings[0]);
- deleteModal.setBody(strings[1]);
- if (!isRepeatedEvent) {
- deleteModal.setSaveButtonText(strings[0]);
- }
-
- deleteModal.getRoot().on(ModalEvents.save, function() {
- CalendarRepository.deleteEvent(eventId, false)
- .then(function() {
- $('body').trigger(CalendarEvents.deleted, [eventId, false]);
- summaryModal.hide();
- return;
- })
- .catch(Notification.exception);
- });
-
- deleteModal.getRoot().on(CalendarEvents.deleteAll, function() {
- CalendarRepository.deleteEvent(eventId, true)
- .then(function() {
- $('body').trigger(CalendarEvents.deleted, [eventId, true]);
- summaryModal.hide();
- return;
- })
- .catch(Notification.exception);
- });
-
- return deleteModal;
- })
- .fail(Notification.exception);
- }
-
// Automatically register with the modal registry the first time this module is imported so that you can create modals
// of this type using the modal factory.
if (!registered) {
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define(['jquery', 'core/templates', 'core/notification', 'core_calendar/repository', 'core_calendar/events'],
- function($, Templates, Notification, CalendarRepository, CalendarEvents) {
+define([
+ 'jquery',
+ 'core/templates',
+ 'core/notification',
+ 'core_calendar/repository',
+ 'core_calendar/events',
+ 'core_calendar/selectors',
+], function(
+ $,
+ Templates,
+ Notification,
+ CalendarRepository,
+ CalendarEvents,
+ CalendarSelectors
+) {
var SELECTORS = {
- ROOT: "[data-region='calendar']",
CALENDAR_NAV_LINK: ".calendarwrapper .arrow_link",
- CALENDAR_MONTH_WRAPPER: ".calendarwrapper",
LOADING_ICON_CONTAINER: '[data-region="overlay-icon-container"]'
};
root = $(root);
root.on('click', SELECTORS.CALENDAR_NAV_LINK, function(e) {
- var courseId = $(root).find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
+ var wrapper = root.find(CalendarSelectors.wrapper);
+ var view = wrapper.data('view');
+ var courseId = wrapper.data('courseid');
+ var categoryId = wrapper.data('categoryid');
var link = $(e.currentTarget);
- changeMonth(root, link.attr('href'), link.data('year'), link.data('month'), courseId);
- e.preventDefault();
+ if (view === 'month') {
+ changeMonth(root, link.attr('href'), link.data('year'), link.data('month'), courseId, categoryId);
+ e.preventDefault();
+ } else if (view === 'day') {
+ changeDay(root, link.attr('href'), link.data('year'), link.data('month'), link.data('day'),
+ courseId, categoryId);
+ e.preventDefault();
+ }
+
});
};
* @param {Number} year Year
* @param {Number} month Month
* @param {Number} courseid The id of the course whose events are shown
+ * @param {Number} categoryid The id of the category whose events are shown
* @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
* @return {promise}
*/
- var refreshMonthContent = function(root, year, month, courseid, target) {
+ var refreshMonthContent = function(root, year, month, courseid, categoryid, target) {
startLoading(root);
- target = target || root.find(SELECTORS.CALENDAR_MONTH_WRAPPER);
+ target = target || root.find(CalendarSelectors.wrapper);
M.util.js_pending([root.get('id'), year, month, courseid].join('-'));
var includenavigation = root.data('includenavigation');
- return CalendarRepository.getCalendarMonthData(year, month, courseid, includenavigation)
+ return CalendarRepository.getCalendarMonthData(year, month, courseid, categoryid, includenavigation)
.then(function(context) {
return Templates.render(root.attr('data-template'), context);
})
/**
* Handle changes to the current calendar view.
*
- * @param {object} root The root element.
+ * @param {object} root The container element
* @param {String} url The calendar url to be shown
* @param {Number} year Year
* @param {Number} month Month
* @param {Number} courseid The id of the course whose events are shown
+ * @param {Number} categoryid The id of the category whose events are shown
* @return {promise}
*/
- var changeMonth = function(root, url, year, month, courseid) {
- return refreshMonthContent(root, year, month, courseid)
+ var changeMonth = function(root, url, year, month, courseid, categoryid) {
+ return refreshMonthContent(root, year, month, courseid, categoryid)
.then(function() {
if (url.length && url !== '#') {
window.history.pushState({}, '', url);
return arguments;
})
.then(function() {
- $('body').trigger(CalendarEvents.monthChanged, [year, month, courseid]);
+ $('body').trigger(CalendarEvents.monthChanged, [year, month, courseid, categoryid]);
return arguments;
});
};
*
* @param {object} root The container element.
* @param {Number} courseId The course id.
+ * @param {Number} categoryId The id of the category whose events are shown
* @return {promise}
*/
- var reloadCurrentMonth = function(root, courseId) {
- var year = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('year');
- var month = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('month');
+ var reloadCurrentMonth = function(root, courseId, categoryId) {
+ var year = root.find(CalendarSelectors.wrapper).data('year');
+ var month = root.find(CalendarSelectors.wrapper).data('month');
+
+ if (typeof courseId === 'undefined') {
+ courseId = root.find(CalendarSelectors.wrapper).data('courseid');
+ }
+
+ if (typeof categoryId === 'undefined') {
+ categoryId = root.find(CalendarSelectors.wrapper).data('categoryid');
+ }
+
+ return refreshMonthContent(root, year, month, courseId, categoryId);
+ };
+
+
+ /**
+ * Refresh the day content.
+ *
+ * @param {object} root The root element.
+ * @param {Number} year Year
+ * @param {Number} month Month
+ * @param {Number} day Day
+ * @param {Number} courseid The id of the course whose events are shown
+ * @param {Number} categoryId The id of the category whose events are shown
+ * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
+ * @return {promise}
+ */
+ var refreshDayContent = function(root, year, month, day, courseid, categoryId, target) {
+ startLoading(root);
+
+ target = target || root.find(CalendarSelectors.wrapper);
+
+ M.util.js_pending([root.get('id'), year, month, day, courseid, categoryId].join('-'));
+ var includenavigation = root.data('includenavigation');
+ return CalendarRepository.getCalendarDayData(year, month, day, courseid, categoryId, includenavigation)
+ .then(function(context) {
+ return Templates.render(root.attr('data-template'), context);
+ })
+ .then(function(html, js) {
+ return Templates.replaceNode(target, html, js);
+ })
+ .then(function() {
+ $('body').trigger(CalendarEvents.viewUpdated);
+ return;
+ })
+ .always(function() {
+ M.util.js_complete([root.get('id'), year, month, day, courseid, categoryId].join('-'));
+ return stopLoading(root);
+ })
+ .fail(Notification.exception);
+ };
+
+ /**
+ * Reload the current day view data.
+ *
+ * @param {object} root The container element.
+ * @param {Number} courseId The course id.
+ * @param {Number} categoryId The id of the category whose events are shown
+ * @return {promise}
+ */
+ var reloadCurrentDay = function(root, courseId, categoryId) {
+ var wrapper = root.find(CalendarSelectors.wrapper);
+ var year = wrapper.data('year');
+ var month = wrapper.data('month');
+ var day = wrapper.data('day');
if (!courseId) {
- courseId = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
+ courseId = root.find(CalendarSelectors.wrapper).data('courseid');
}
- return refreshMonthContent(root, year, month, courseId);
+
+ if (typeof categoryId === 'undefined') {
+ categoryId = root.find(CalendarSelectors.wrapper).data('categoryid');
+ }
+
+ return refreshDayContent(root, year, month, day, courseId, categoryId);
+ };
+
+ /**
+ * Handle changes to the current calendar view.
+ *
+ * @param {object} root The root element.
+ * @param {String} url The calendar url to be shown
+ * @param {Number} year Year
+ * @param {Number} month Month
+ * @param {Number} day Day
+ * @param {Number} courseId The id of the course whose events are shown
+ * @param {Number} categoryId The id of the category whose events are shown
+ * @return {promise}
+ */
+ var changeDay = function(root, url, year, month, day, courseId, categoryId) {
+ return refreshDayContent(root, year, month, day, courseId, categoryId)
+ .then(function() {
+ if (url.length && url !== '#') {
+ window.history.pushState({}, '', url);
+ }
+ return arguments;
+ })
+ .then(function() {
+ $('body').trigger(CalendarEvents.dayChanged, [year, month, day, courseId, categoryId]);
+ return arguments;
+ });
};
/**
loadingIconContainer.addClass('hidden');
};
+ /**
+ * Reload the current month view data.
+ *
+ * @param {object} root The container element.
+ * @param {Number} courseId The course id.
+ * @return {promise}
+ */
+ var reloadCurrentUpcoming = function(root, courseId) {
+ startLoading(root);
+
+ var target = root.find(CalendarSelectors.wrapper);
+
+ if (!courseId) {
+ courseId = root.find(CalendarSelectors.wrapper).data('courseid');
+ }
+
+ return CalendarRepository.getCalendarUpcomingData(courseId)
+ .then(function(context) {
+ return Templates.render(root.attr('data-template'), context);
+ })
+ .then(function(html, js) {
+ window.history.replaceState(null, null, '?view=upcoming&course=' + courseId);
+ return Templates.replaceNode(target, html, js);
+ })
+ .then(function() {
+ $('body').trigger(CalendarEvents.viewUpdated);
+ return;
+ })
+ .always(function() {
+ return stopLoading(root);
+ })
+ .fail(Notification.exception);
+ };
+
return {
init: function(root) {
registerEventListeners(root);
},
reloadCurrentMonth: reloadCurrentMonth,
changeMonth: changeMonth,
- refreshMonthContent: refreshMonthContent
+ refreshMonthContent: refreshMonthContent,
+ reloadCurrentDay: reloadCurrentDay,
+ changeDay: changeDay,
+ refreshDayContent: refreshDayContent,
+ reloadCurrentUpcoming: reloadCurrentUpcoming
};
});
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains event class for displaying the day view.
+ *
+ * @package core_calendar
+ * @copyright 2017 Simey Lameze <simey@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_calendar\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+use renderer_base;
+use moodle_url;
+use \core_calendar\local\event\container;
+
+/**
+ * Class for displaying the day view.
+ *
+ * @package core_calendar
+ * @copyright 2017 Simey Lameze <simey@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class calendar_day_exporter extends exporter {
+ /**
+ * @var \calendar_information $calendar The calendar to be rendered.
+ */
+ protected $calendar;
+
+ /**
+ * @var moodle_url $url The URL for the day view page.
+ */
+ protected $url;
+
+ /**
+ * Constructor for day exporter.
+ *
+ * @param \calendar_information $calendar The calendar being represented.
+ * @param array $related The related information
+ */
+ public function __construct(\calendar_information $calendar, $related) {
+ $this->calendar = $calendar;
+
+ parent::__construct([], $related);
+ }
+
+ /**
+ * Return the list of additional properties.
+ *
+ * @return array
+ */
+ protected static function define_other_properties() {
+ return [
+ 'events' => [
+ 'type' => calendar_event_exporter::read_properties_definition(),
+ 'multiple' => true,
+ ],
+ 'defaulteventcontext' => [
+ 'type' => PARAM_INT,
+ 'default' => null,
+ ],
+ 'filter_selector' => [
+ 'type' => PARAM_RAW,
+ ],
+ 'courseid' => [
+ 'type' => PARAM_INT,
+ ],
+ 'categoryid' => [
+ 'type' => PARAM_INT,
+ 'optional' => true,
+ 'default' => 0,
+ ],
+ 'neweventtimestamp' => [
+ 'type' => PARAM_INT,
+ ],
+ 'date' => [
+ 'type' => date_exporter::read_properties_definition(),
+ ],
+ 'periodname' => [
+ // Note: We must use RAW here because the calendar type returns the formatted month name based on a
+ // calendar format.
+ 'type' => PARAM_RAW,
+ ],
+ 'previousperiod' => [
+ 'type' => date_exporter::read_properties_definition(),
+ ],
+ 'previousperiodlink' => [
+ 'type' => PARAM_URL,
+ ],
+ 'previousperiodname' => [
+ // Note: We must use RAW here because the calendar type returns the formatted month name based on a
+ // calendar format.
+ 'type' => PARAM_RAW,
+ ],
+ 'nextperiod' => [
+ 'type' => date_exporter::read_properties_definition(),
+ ],
+ 'nextperiodname' => [
+ // Note: We must use RAW here because the calendar type returns the formatted month name based on a
+ // calendar format.
+ 'type' => PARAM_RAW,
+ ],
+ 'nextperiodlink' => [
+ 'type' => PARAM_URL,
+ ],
+ 'larrow' => [
+ // The left arrow defined by the theme.
+ 'type' => PARAM_RAW,
+ ],
+ 'rarrow' => [
+ // The right arrow defined by the theme.
+ 'type' => PARAM_RAW,
+ ],
+ ];
+ }
+
+ /**
+ * Get the additional values to inject while exporting.
+ *
+ * @param renderer_base $output The renderer.
+ * @return array Keys are the property names, values are their values.
+ */
+ protected function get_other_values(renderer_base $output) {
+ $timestamp = $this->calendar->time;
+
+ $cache = $this->related['cache'];
+ $url = new moodle_url('/calendar/view.php', [
+ 'view' => 'day',
+ 'time' => $timestamp,
+ ]);
+ if ($this->calendar->course && SITEID !== $this->calendar->course->id) {
+ $url->param('course', $this->calendar->course->id);
+ } else if ($this->calendar->categoryid) {
+ $url->param('category', $this->calendar->categoryid);
+ }
+ $this->url = $url;
+ $return['events'] = array_map(function($event) use ($cache, $output, $url) {
+ $context = $cache->get_context($event);
+ $course = $cache->get_course($event);
+ $exporter = new calendar_event_exporter($event, [
+ 'context' => $context,
+ 'course' => $course,
+ 'daylink' => $url,
+ 'type' => $this->related['type'],
+ 'today' => $this->calendar->time,
+ ]);
+
+ $data = $exporter->export($output);
+
+ // We need to override default formatted time because it differs from day view.
+ // Formatted time for day view adds a link to the day view.
+ $legacyevent = container::get_event_mapper()->from_event_to_legacy_event($event);
+ $data->formattedtime = calendar_format_event_time($legacyevent, time(), null);
+
+ return $data;
+ }, $this->related['events']);
+
+ if ($context = $this->get_default_add_context()) {
+ $return['defaulteventcontext'] = $context->id;
+ }
+
+ if ($this->calendar->categoryid) {
+ $return['categoryid'] = $this->calendar->categoryid;
+ }
+
+ $return['filter_selector'] = $this->get_course_filter_selector($output);
+ $return['courseid'] = $this->calendar->courseid;
+
+ $previousperiod = $this->get_previous_day_data();
+ $nextperiod = $this->get_next_day_data();
+ $date = $this->related['type']->timestamp_to_date_array($this->calendar->time);
+
+ $nextperiodlink = new moodle_url($this->url);
+ $nextperiodlink->param('time', $nextperiod[0]);
+
+ $previousperiodlink = new moodle_url($this->url);
+ $previousperiodlink->param('time', $previousperiod[0]);
+
+ $days = calendar_get_days();
+ $return['date'] = (new date_exporter($date))->export($output);
+ $return['periodname'] = userdate($this->calendar->time, get_string('strftimedaydate'));
+ $return['previousperiod'] = (new date_exporter($previousperiod))->export($output);
+ $return['previousperiodname'] = $days[$previousperiod['wday']]['fullname'];
+ $return['previousperiodlink'] = $previousperiodlink->out(false);
+ $return['nextperiod'] = (new date_exporter($nextperiod))->export($output);
+ $return['nextperiodname'] = $days[$nextperiod['wday']]['fullname'];
+ $return['nextperiodlink'] = $nextperiodlink->out(false);
+ $return['larrow'] = $output->larrow();
+ $return['rarrow'] = $output->rarrow();
+
+ // Need to account for user's timezone.
+ $usernow = usergetdate(time());
+ $today = new \DateTimeImmutable();
+ $neweventtimestamp = $today->setTimestamp($date[0])->setTime(
+ $usernow['hours'],
+ $usernow['minutes'],
+ $usernow['seconds']
+ );
+ $return['neweventtimestamp'] = $neweventtimestamp->getTimestamp();
+
+ return $return;
+ }
+
+ /**
+ * Get the default context for use when adding a new event.
+ *
+ * @return null|\context
+ */
+ protected function get_default_add_context() {
+ if (calendar_user_can_add_event($this->calendar->course)) {
+ return \context_course::instance($this->calendar->course->id);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the course filter selector.
+ *
+ * @param renderer_base $output
+ * @return string The html code for the course filter selector.
+ */
+ protected function get_course_filter_selector(renderer_base $output) {
+ $langstr = get_string('upcomingeventsfor', 'calendar');
+ return $output->course_filter_selector($this->url, $langstr);
+ }
+
+ /**
+ * Returns a list of objects that are related.
+ *
+ * @return array
+ */
+ protected static function define_related() {
+ return [
+ 'events' => '\core_calendar\local\event\entities\event_interface[]',
+ 'cache' => '\core_calendar\external\events_related_objects_cache',
+ 'type' => '\core_calendar\type_base',
+ ];
+ }
+
+ /**
+ * Get the previous day timestamp.
+ *
+ * @return int The previous day timestamp.
+ */
+ protected function get_previous_day_data() {
+ $type = $this->related['type'];
+ $time = $type->get_prev_day($this->calendar->time);
+
+ return $type->timestamp_to_date_array($time);
+ }
+
+ /**
+ * Get the next day timestamp.
+ *
+ * @return int The next day timestamp.
+ */
+ protected function get_next_day_data() {
+ $type = $this->related['type'];
+ $time = $type->get_next_day($this->calendar->time);
+
+ return $type->timestamp_to_date_array($time);
+ }
+}
$params = array('update' => $moduleid, 'return' => true, 'sesskey' => sesskey());
$editurl = new \moodle_url('/course/mod.php', $params);
$values['editurl'] = $editurl->out(false);
+ } else if ($event->get_type() == 'category') {
+ $url = $event->get_category()->get_proxied_instance()->get_view_link();
} else if ($event->get_type() == 'course') {
$url = course_get_url($event->get_course()->get('id') ?: SITEID);
} else {
}
}
+ // Include category name into the event name, if applicable.
+ $proxy = $this->event->get_category();
+ if ($proxy && $proxy->get('id')) {
+ $category = $proxy->get_proxied_instance();
+ $eventnameparams = (object) [
+ 'name' => $values['popupname'],
+ 'category' => $category->get_formatted_name(),
+ ];
+ $values['popupname'] = get_string('eventnameandcategory', 'calendar', $eventnameparams);
+ }
+
// Include course's shortname into the event name, if applicable.
$course = $this->event->get_course();
if ($course && $course->get('id') && $course->get('id') !== SITEID) {
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains event class for displaying the upcoming view.
+ *
+ * @package core_calendar
+ * @copyright 2017 Simey Lameze <simey@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_calendar\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+use renderer_base;
+use moodle_url;
+use \core_calendar\local\event\container;
+
+/**
+ * Class for displaying the day view.
+ *
+ * @package core_calendar
+ * @copyright 2017 Simey Lameze <simey@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class calendar_upcoming_exporter extends exporter {
+ /**
+ * @var \calendar_information $calendar The calendar to be rendered.
+ */
+ protected $calendar;
+
+ /**
+ * @var moodle_url $url The URL for the upcoming view page.
+ */
+ protected $url;
+
+ /**
+ * Constructor for upcoming exporter.
+ *
+ * @param \calendar_information $calendar The calendar being represented.
+ * @param array $related The related information
+ */
+ public function __construct(\calendar_information $calendar, $related) {
+ $this->calendar = $calendar;
+
+ parent::__construct([], $related);
+ }
+
+ /**
+ * Return the list of additional properties.
+ *
+ * @return array
+ */
+ protected static function define_other_properties() {
+ return [
+ 'events' => [
+ 'type' => calendar_event_exporter::read_properties_definition(),
+ 'multiple' => true,
+ ],
+ 'defaulteventcontext' => [
+ 'type' => PARAM_INT,
+ 'default' => null,
+ ],
+ 'filter_selector' => [
+ 'type' => PARAM_RAW,
+ ],
+ 'courseid' => [
+ 'type' => PARAM_INT,
+ ],
+ ];
+ }
+
+ /**
+ * Get the additional values to inject while exporting.
+ *
+ * @param renderer_base $output The renderer.
+ * @return array Keys are the property names, values are their values.
+ */
+ protected function get_other_values(renderer_base $output) {
+ $timestamp = $this->calendar->time;
+
+ $cache = $this->related['cache'];
+ $url = new moodle_url('/calendar/view.php', [
+ 'view' => 'upcoming',
+ 'time' => $timestamp,
+ 'course' => $this->calendar->course->id,
+ ]);
+ $this->url = $url;
+ $return['events'] = array_map(function($event) use ($cache, $output, $url) {
+ $context = $cache->get_context($event);
+ $course = $cache->get_course($event);
+ $exporter = new calendar_event_exporter($event, [
+ 'context' => $context,
+ 'course' => $course,
+ 'daylink' => $url,
+ 'type' => $this->related['type'],
+ 'today' => $this->calendar->time,
+ ]);
+
+ $data = $exporter->export($output);
+
+ // We need to override default formatted time because it differs from day view.
+ // Formatted time for upcoming view adds a link to the day view.
+ $legacyevent = container::get_event_mapper()->from_event_to_legacy_event($event);
+ $data->formattedtime = calendar_format_event_time($legacyevent, time(), null);
+
+ return $data;
+ }, $this->related['events']);
+
+ if ($context = $this->get_default_add_context()) {
+ $return['defaulteventcontext'] = $context->id;
+ }
+ $return['filter_selector'] = $this->get_course_filter_selector($output);
+ $return['courseid'] = $this->calendar->courseid;
+ return $return;
+ }
+
+ /**
+ * Get the default context for use when adding a new event.
+ *
+ * @return null|\context
+ */
+ protected function get_default_add_context() {
+ if (calendar_user_can_add_event($this->calendar->course)) {
+ return \context_course::instance($this->calendar->course->id);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the course filter selector.
+ *
+ * @param renderer_base $output
+ * @return string The html code for the course filter selector.
+ */
+ protected function get_course_filter_selector(renderer_base $output) {
+ $langstr = get_string('upcomingeventsfor', 'calendar');
+ return $output->course_filter_selector($this->url, $langstr);
+ }
+
+ /**
+ * Returns a list of objects that are related.
+ *
+ * @return array
+ */
+ protected static function define_related() {
+ return [
+ 'events' => '\core_calendar\local\event\entities\event_interface[]',
+ 'cache' => '\core_calendar\external\events_related_objects_cache',
+ 'type' => '\core_calendar\type_base',
+ ];
+ }
+}
*/
public function __construct(\calendar_information $calendar, $data, $related) {
$this->calendar = $calendar;
- $this->url = new moodle_url('/calendar/view.php', [
- 'view' => 'day',
- 'time' => $calendar->time,
- 'course' => $this->calendar->course->id,
- ]);
+
+ $url = new moodle_url('/calendar/view.php', [
+ 'view' => 'day',
+ 'time' => $calendar->time,
+ ]);
+
+ if ($this->calendar->course && SITEID !== $this->calendar->course->id) {
+ $url->param('course', $this->calendar->course->id);
+ } else if ($this->calendar->categoryid) {
+ $url->param('category', $this->calendar->categoryid);
+ }
+
+ $this->url = $url;
+
parent::__construct($data, $related);
}
'navigation' => $this->get_navigation(),
'filter_selector' => $this->get_course_filter_selector($output),
'new_event_button' => $this->get_new_event_button(),
+ 'viewdaylink' => $this->url->out(false),
];
- $return['viewdaylink'] = $this->url->out(false);
$cache = $this->related['cache'];
$eventexporters = array_map(function($event) use ($cache, $output) {
* @return array
*/
protected static function define_other_properties() {
-
$values = parent::define_other_properties();
+
$values['url'] = ['type' => PARAM_URL];
$values['action'] = [
'type' => event_action_exporter::read_properties_definition(),
'optional' => true,
];
- $values['editurl'] = [
- 'type' => PARAM_URL,
- 'optional' => true,
- ];
return $values;
}
$params = array('update' => $moduleid, 'return' => true, 'sesskey' => sesskey());
$editurl = new \moodle_url('/course/mod.php', $params);
$values['editurl'] = $editurl->out(false);
+ } else if ($event->get_type() == 'category') {
+ $url = $event->get_category()->get_proxied_instance()->get_view_link();
} else if ($event->get_type() == 'course') {
$url = \course_get_url($this->related['course'] ?: SITEID);
} else {
use \core_calendar\local\event\entities\event_interface;
use \core_calendar\local\event\entities\action_event_interface;
use \core_course\external\course_summary_exporter;
+use \core\external\coursecat_summary_exporter;
use \renderer_base;
use moodle_url;
$endtimestamp = $event->get_times()->get_end_time()->getTimestamp();
$groupid = $event->get_group() ? $event->get_group()->get('id') : null;
$userid = $event->get_user() ? $event->get_user()->get('id') : null;
+ $categoryid = $event->get_category() ? $event->get_category()->get('id') : null;
$data = new \stdClass();
$data->id = $event->get_id();
$data->descriptionformat = $event->get_description()->get_format();
$data->groupid = $groupid;
$data->userid = $userid;
+ $data->categoryid = $categoryid;
$data->eventtype = $event->get_type();
$data->timestart = $starttimestamp;
$data->timeduration = $endtimestamp - $starttimestamp;
'default' => null,
'null' => NULL_ALLOWED
],
+ 'categoryid' => [
+ 'type' => PARAM_INT,
+ 'optional' => true,
+ 'default' => null,
+ 'null' => NULL_ALLOWED
+ ],
'groupid' => [
'type' => PARAM_INT,
'optional' => true,
'icon' => [
'type' => event_icon_exporter::read_properties_definition(),
],
+ 'category' => [
+ 'type' => coursecat_summary_exporter::read_properties_definition(),
+ 'optional' => true,
+ ],
'course' => [
'type' => course_summary_exporter::read_properties_definition(),
'optional' => true,
'iscourseevent' => [
'type' => PARAM_BOOL
],
+ 'iscategoryevent' => [
+ 'type' => PARAM_BOOL
+ ],
'groupname' => [
'type' => PARAM_RAW,
'optional' => true,
$context = $this->related['context'];
$values['isactionevent'] = false;
$values['iscourseevent'] = false;
+ $values['iscategoryevent'] = false;
if ($moduleproxy = $event->get_course_module()) {
$values['isactionevent'] = true;
} else if ($event->get_type() == 'course') {
$values['iscourseevent'] = true;
+ } else if ($event->get_type() == 'category') {
+ $values['iscategoryevent'] = true;
}
$timesort = $event->get_times()->get_sort_time()->getTimestamp();
$iconexporter = new event_icon_exporter($event, ['context' => $context]);
$subscriptionexporter = new event_subscription_exporter($event);
$values['subscription'] = $subscriptionexporter->export($output);
+ $proxy = $this->event->get_category();
+ if ($proxy && $proxy->get('id')) {
+ $category = $proxy->get_proxied_instance();
+ $categorysummaryexporter = new coursecat_summary_exporter($category, ['context' => $context]);
+ $values['category'] = $categorysummaryexporter->export($output);
+ }
+
if ($course = $this->related['course']) {
$coursesummaryexporter = new course_summary_exporter($course, ['context' => $context]);
$values['course'] = $coursesummaryexporter->export($output);
*/
public function __construct(event_interface $event, $related = []) {
$coursemodule = $event->get_course_module();
+ $category = $event->get_category();
+ $categoryid = $category ? $category->get('id') : null;
$course = $event->get_course();
$courseid = $course ? $course->get('id') : null;
$group = $event->get_group();
$userid = $user ? $user->get('id') : null;
$isactivityevent = !empty($coursemodule);
$isglobalevent = ($course && $courseid == SITEID);
+ $iscategoryevent = ($category && !empty($categoryid));
$iscourseevent = ($course && !empty($courseid) && $courseid != SITEID && empty($groupid));
$isgroupevent = ($group && !empty($groupid));
$isuserevent = ($user && !empty($userid));
} else if ($isglobalevent) {
$key = 'i/siteevent';
$component = 'core';
- $alttext = get_string('globalevent', 'calendar');
+ $alttext = get_string('typesite', 'calendar');
+ } else if ($iscategoryevent) {
+ $key = 'i/categoryevent';
+ $component = 'core';
+ $alttext = get_string('typecategory', 'calendar');
} else if ($iscourseevent) {
$key = 'i/courseevent';
$component = 'core';
- $alttext = get_string('courseevent', 'calendar');
+ $alttext = get_string('typecourse', 'calendar');
} else if ($isgroupevent) {
$key = 'i/groupevent';
$component = 'core';
- $alttext = get_string('groupevent', 'calendar');
+ $alttext = get_string('typegroup', 'calendar');
} else if ($isuserevent) {
$key = 'i/userevent';
$component = 'core';
- $alttext = get_string('userevent', 'calendar');
+ $alttext = get_string('typeuser', 'calendar');
} else {
// Default to site event icon?
$key = 'i/siteevent';
$component = 'core';
- $alttext = get_string('globalevent', 'calendar');
+ $alttext = get_string('typesite', 'calendar');
}
$data = new \stdClass();
return new \single_button($exportcalendarurl, get_string('exportcalendar', 'calendar'));
}
- /**
- * Get the iCal url.
- *
- * @return string The iCal url.
- */
- protected function get_ical_url() {
- if ($this->token) {
- return new moodle_url('/calendar/export_execute.php', ['preset_what' => 'all',
- 'preset_time' => 'recentupcoming', 'userid' => $this->userid, 'authtoken' => $this->token]);
- }
- }
-
/**
* Get manage subscription button.
*
if ($managesubscriptionbutton = $this->get_manage_subscriptions_button()) {
$values->managesubscriptionbutton = $managesubscriptionbutton->export_for_template($output);
}
- $values->icalurl = $this->get_ical_url()->out(false);
}
return (array) $values;
'type' => PARAM_RAW,
'default' => null,
],
- 'icalurl' => [
- 'type' => PARAM_URL,
- 'default' => null,
- ],
);
}
}
'time' => $calendar->time,
]);
- if ($this->calendar->courseid) {
- $this->url->param('course', $this->calendar->courseid);
+ if ($this->calendar->course && SITEID !== $this->calendar->course->id) {
+ $this->url->param('course', $this->calendar->course->id);
+ } else if ($this->calendar->categoryid) {
+ $this->url->param('category', $this->calendar->categoryid);
}
$related['type'] = $type;
'courseid' => [
'type' => PARAM_INT,
],
+ 'categoryid' => [
+ 'type' => PARAM_INT,
+ 'optional' => true,
+ 'default' => 0,
+ ],
'filter_selector' => [
'type' => PARAM_RAW,
],
$return['defaulteventcontext'] = $context->id;
}
+ if ($this->calendar->categoryid) {
+ $return['categoryid'] = $this->calendar->categoryid;
+ }
+
return $return;
}
$url = new moodle_url('/calendar/view.php', [
'view' => 'day',
'time' => $timestamp,
- 'course' => $this->calendar->course->id,
- ]);
+ ]);
+
+ if ($this->calendar->course && SITEID !== $this->calendar->course->id) {
+ $url->param('course', $this->calendar->course->id);
+ } else if ($this->calendar->categoryid) {
+ $url->param('category', $this->calendar->categoryid);
+ }
$return['viewdaylink'] = $url->out(false);
+
if ($popovertitle = $this->get_popover_title()) {
$return['popovertitle'] = $popovertitle;
}
array $usersfilter = null,
array $groupsfilter = null,
array $coursesfilter = null,
+ array $categoriesfilter = null,
$withduration = true,
$ignorehidden = true,
callable $filter = null
$usersfilter,
$groupsfilter,
$coursesfilter,
+ $categoriesfilter,
$withduration,
$ignorehidden,
$filter
[self::class, 'apply_component_provide_event_action'],
[self::class, 'apply_component_is_event_visible'],
function ($dbrow) {
+ if (!empty($dbrow->categoryid)) {
+ // This is a category event. Check that the category is visible to this user.
+ $category = \coursecat::get($dbrow->categoryid, IGNORE_MISSING, true);
+
+ if (empty($category) || !$category->is_uservisible()) {
+ return true;
+ }
+ }
+
// At present we only have a bail-out check for events in course modules.
if (empty($dbrow->modulename)) {
return false;
use core_calendar\local\event\factories\event_factory_interface;
use core_calendar\local\event\strategies\raw_event_retrieval_strategy_interface;
+require_once($CFG->libdir . '/coursecatlib.php');
+
/**
* Event vault class.
*
array $usersfilter = null,
array $groupsfilter = null,
array $coursesfilter = null,
+ array $categoriesfilter = null,
$withduration = true,
$ignorehidden = true,
callable $filter = null
$usersfilter,
$groupsfilter,
$coursesfilter,
+ $categoriesfilter,
$where,
$params,
"COALESCE(e.timesort, e.timestart) ASC, e.id ASC",
event_interface $afterevent = null,
$limitnum = 20
) {
+ $categoryids = array_map(function($category) {
+ return $category->id;
+ }, \coursecat::get_all());
+
$courseids = array_map(function($course) {
return $course->id;
}, enrol_get_all_users_courses($user->id));
[$user->id],
$groupids ? $groupids : null,
$courseids ? $courseids : null,
+ $categoryids ? $categoryids : null,
true,
true,
function ($event) {
[$user->id],
$groupings[0] ? $groupings[0] : null,
[$course->id],
+ [],
true,
true,
function ($event) use ($course) {
[$userid],
null,
null,
+ null,
$whereconditions,
$whereparams,
$ordersql,
array $usersfilter = null,
array $groupsfilter = null,
array $coursesfilter = null,
+ array $categoriesfilter = null,
$withduration = true,
$ignorehidden = true,
callable $filter = null
*/
protected $action;
+ /**
+ * @var proxy_interface $category Category for this event.
+ */
+ protected $category;
+
/**
* Constructor.
*
return $this->event->get_description();
}
+ public function get_category() {
+ return $this->event->get_category();
+ }
+
public function get_course() {
return $this->event->get_course();
}
*/
protected $description;
+ /**
+ * @var proxy_interface $category Category for this event.
+ */
+ protected $category;
+
/**
* @var proxy_interface $course Course for this event.
*/
* @param int $id The event's ID in the database.
* @param string $name The event's name.
* @param description_interface $description The event's description.
+ * @param proxy_interface $category The category associated with the event.
* @param proxy_interface $course The course associated with the event.
* @param proxy_interface $group The group associated with the event.
* @param proxy_interface $user The user associated with the event.
$id,
$name,
description_interface $description,
+ proxy_interface $category = null,
proxy_interface $course = null,
proxy_interface $group = null,
proxy_interface $user = null,
$this->id = $id;
$this->name = $name;
$this->description = $description;
+ $this->category = $category;
$this->course = $course;
$this->group = $group;
$this->user = $user;
return $this->description;
}
+ public function get_category() {
+ return $this->category;
+ }
+
public function get_course() {
return $this->course;
}
*/
public function get_description();
+ /**
+ * Get the category object associated with the event.
+ *
+ * @return proxy_interface
+ */
+ public function get_category();
+
/**
* Get the course object associated with the event.
*
* @param int $start Start offset.
* @return \stdClass[]
*/
- protected function load_event_records($start = 1) {
+ protected function load_event_records($start = 0) {
global $DB;
- while ($records = $DB->get_records(
+ while ($records = $DB->get_records_select(
'event',
- ['repeatid' => $this->parentid],
- '',
+ 'id <> :parentid AND repeatid = :repeatid',
+ [
+ 'parentid' => $this->parentid,
+ 'repeatid' => $this->parentid,
+ ],
+ 'id ASC',
'*',
$start,
self::DB_QUERY_LIMIT
use core_calendar\local\event\entities\repeat_event_collection;
use core_calendar\local\event\exceptions\invalid_callback_exception;
use core_calendar\local\event\proxies\cm_info_proxy;
+use core_calendar\local\event\proxies\coursecat_proxy;
use core_calendar\local\event\proxies\std_proxy;
use core_calendar\local\event\value_objects\event_description;
use core_calendar\local\event\value_objects\event_times;
use core_calendar\local\event\entities\event_interface;
+require_once($CFG->libdir . '/coursecatlib.php');
+
/**
* Abstract factory for creating calendar events.
*
return null;
}
+ $category = null;
$course = null;
$group = null;
$user = null;
$module = new cm_info_proxy($dbrow->modulename, $dbrow->instance, $dbrow->courseid);
}
+ $category = new coursecat_proxy($dbrow->categoryid);
+
$course = new std_proxy($dbrow->courseid, function($id) {
return calendar_get_course_cached($this->coursecachereference, $id);
});
$dbrow->id,
$dbrow->name,
new event_description($dbrow->description, $dbrow->format),
+ $category,
$course,
$group,
$user,
/**
* The form definition
*/
- public function definition () {
+ public function definition() {
global $PAGE;
$mform = $this->_form;
if (isset($eventtypes['course'])) {
$options['course'] = get_string('course');
}
+ if (isset($eventtypes['category'])) {
+ $options['category'] = get_string('category');
+ }
if (isset($eventtypes['site'])) {
$options['site'] = get_string('site');
}
$mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
}
+ if (isset($eventtypes['category'])) {
+ $categoryoptions = [];
+ foreach ($eventtypes['category'] as $id => $category) {
+ $categoryoptions[$id] = $category;
+ }
+
+ $mform->addElement('select', 'categoryid', get_string('category'), $categoryoptions);
+ $mform->hideIf('categoryid', 'eventtype', 'noteq', 'category');
+ }
+
if (isset($eventtypes['course'])) {
$courseoptions = [];
foreach ($eventtypes['course'] as $course) {
'name' => $coalesce('name'),
'description' => $coalesce('description'),
'format' => $coalesce('format'),
+ 'categoryid' => $coalesce('categoryid'),
'courseid' => $coalesce('courseid'),
'groupid' => $coalesce('groupid'),
'userid' => $coalesce('userid'),
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course category proxy.
+ *
+ * @package core_calendar
+ * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_calendar\local\event\proxies;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/coursecatlib.php');
+
+/**
+ * Course category proxy.
+ *
+ * This returns an instance of a coursecat rather than a stdClass.
+ *
+ * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class coursecat_proxy implements proxy_interface {
+ /**
+ * @var int $id The ID of the database record.
+ */
+ protected $id;
+
+ /**
+ * @var \stdClass $base Base class to get members from.
+ */
+ protected $base;
+
+ /**
+ * @var \coursecat $category The proxied instance.
+ */
+ protected $category;
+
+ /**
+ * coursecat_proxy constructor.
+ *
+ * @param int $id The ID of the record in the database.
+ */
+ public function __construct($id) {
+ $this->id = $id;
+ $this->base = (object) [
+ 'id' => $id,
+ ];
+ }
+
+ /**
+ * Retrieve a member of the proxied class.
+ *
+ * @param string $member The name of the member to retrieve
+ * @return mixed The member.
+ */
+ public function get($member) {
+ if ($this->base && property_exists($this->base, $member)) {
+ return $this->base->{$member};
+ }
+
+ return $this->get_proxied_instance()->{$member};
+ }
+
+ /**
+ * Get the full instance of the proxied class.
+ *
+ * @return \coursecat
+ */
+ public function get_proxied_instance() : \coursecat {
+ if (!$this->category) {
+ $this->category = \coursecat::get($this->id, IGNORE_MISSING, true);
+ }
+ return $this->category;
+ }
+}
array $usersfilter = null,
array $groupsfilter = null,
array $coursesfilter = null,
+ array $categoriesfilter = null,
array $whereconditions = null,
array $whereparams = null,
$ordersql = null,
!is_null($usersfilter) ? $usersfilter : true, // True means no filter in old implementation.
!is_null($groupsfilter) ? $groupsfilter : true,
!is_null($coursesfilter) ? $coursesfilter : true,
+ !is_null($categoriesfilter) ? $categoriesfilter : true,
$whereconditions,
$whereparams,
$ordersql,
$users,
$groups,
$courses,
+ $categories,
$whereconditions,
$whereparams,
$ordersql,
$params = array();
// Quick test.
- if (empty($users) && empty($groups) && empty($courses)) {
+ if (empty($users) && empty($groups) && empty($courses) && empty($categories)) {
return array();
}
if ((is_array($users) && !empty($users)) or is_numeric($users)) {
// Events from a number of users.
list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED);
- $filters[] = "(e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0)";
+ $filters[] = "(e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)";
$params = array_merge($params, $inparamsusers);
} else if ($users === true) {
// Events from ALL users.
- $filters[] = "(e.userid != 0 AND e.courseid = 0 AND e.groupid = 0)";
+ $filters[] = "(e.userid != 0 AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)";
}
// Boolean false (no users at all): We don't need to do anything.
$filters[] = "(e.groupid = 0 AND e.courseid != 0)";
}
+ // Category filter.
+ if ((is_array($categories) && !empty($categories)) or is_numeric($categories)) {
+ list($insqlcategories, $inparamscategories) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED);
+ $filters[] = "(e.groupid = 0 AND e.courseid = 0 AND e.categoryid $insqlcategories)";
+ $params = array_merge($params, $inparamscategories);
+ } else if ($categories === true) {
+ // Events from ALL categories.
+ $filters[] = "(e.groupid = 0 AND e.courseid = 0 AND e.categoryid != 0)";
+ }
+
// Security check: if, by now, we have NOTHING in $whereclause, then it means
// that NO event-selecting clauses were defined. Thus, we won't be returning ANY
// events no matter what. Allowing the code to proceed might return a completely
if ($user) {
// Set filter condition for the user's events.
- $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0)";
+ $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)";
$subqueryparams['user'] = $user;
foreach ($usercourses as $courseid) {
// Set subquery filter condition for the courses.
if (!empty($subquerycourses)) {
list($incourses, $incoursesparams) = $DB->get_in_or_equal($subquerycourses, SQL_PARAMS_NAMED);
- $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid $incourses)";
+ $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid $incourses AND ev.categoryid = 0)";
$subqueryparams = array_merge($subqueryparams, $incoursesparams);
}
+ // Set subquery filter condition for the categories.
+ if ($categories === true) {
+ $subqueryconditions[] = "(ev.categoryid != 0 AND ev.eventtype = 'category')";
+ } else if (!empty($categories)) {
+ list($incategories, $incategoriesparams) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED);
+ $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid = 0 AND ev.categoryid $incategories)";
+ $subqueryparams = array_merge($subqueryparams, $incategoriesparams);
+ }
+
// Build the WHERE condition for the sub-query.
if (!empty($subqueryconditions)) {
$subquerywhere = 'WHERE ' . implode(" OR ", $subqueryconditions);
* @param array|null $usersfilter Array of users to retrieve events for.
* @param array|null $groupsfilter Array of groups to retrieve events for.
* @param array|null $coursesfilter Array of courses to retrieve events for.
+ * @param array|null $categoriesfilter Array of categories to retrieve events for.
* @param array|null $whereconditions Array of where conditions to restrict results.
* @param array|null $whereparams Array of parameters for $whereconditions.
* @param string|null $ordersql SQL to order results.
array $usersfilter = null,
array $groupsfilter = null,
array $coursesfilter = null,
+ array $categoriesfilter = null,
array $whereconditions = null,
array $whereparams = null,
$ordersql = null,
}
$calendar = new calendar_information(0, 0, 0, $time);
-$calendar->prepare_for_view($course, $courses);
+$calendar->set_sources($course, $courses);
$formoptions = new stdClass;
if ($eventid !== 0) {
$PAGE->set_url($url);
$calendar = new calendar_information(0, 0, 0, $time);
-$calendar->prepare_for_view($course, $courses);
+$calendar->set_sources($course, $courses);
$pagetitle = get_string('export', 'calendar');
(!empty($eventobj->groupid) && in_array($eventobj->groupid, $groups)) ||
(!empty($eventobj->courseid) && in_array($eventobj->courseid, $courses)) ||
($USER->id == $eventobj->userid) ||
- (calendar_edit_event_allowed($eventid))) {
+ (calendar_edit_event_allowed($eventobj))) {
$events[$eventid] = $event;
} else {
$warnings[] = array('item' => $eventid, 'warningcode' => 'nopermissions', 'message' => 'you do not have permissions to view this event');
* @param int $year The year to be shown
* @param int $month The month to be shown
* @param int $courseid The course to be included
+ * @param int $categoryid The category to be included
* @param bool $includenavigation Whether to include navigation
* @return array
*/
- public static function get_calendar_monthly_view($year, $month, $courseid, $includenavigation) {
+ public static function get_calendar_monthly_view($year, $month, $courseid, $categoryid, $includenavigation) {
global $CFG, $DB, $USER, $PAGE;
require_once($CFG->dirroot."/calendar/lib.php");
'year' => $year,
'month' => $month,
'courseid' => $courseid,
+ 'categoryid' => $categoryid,
'includenavigation' => $includenavigation,
]);
+ // TODO: Copy what we do in calendar/view.php.
+ $context = \context_user::instance($USER->id);
+ self::validate_context($context);
+ $PAGE->set_url('/calendar/');
+
if ($courseid != SITEID && !empty($courseid)) {
// Course ID must be valid and existing.
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
$courses = [$course->id => $course];
+
+ $coursecat = \coursecat::get($course->category);
+ $category = $coursecat->get_db_record();
} else {
$course = get_site();
$courses = calendar_get_default_courses();
+ $category = null;
+
+ if ($categoryid) {
+ self::validate_context(context_coursecat::instance($categoryid));
+ $ids = [$categoryid];
+ $category = \coursecat::get($categoryid);
+ $ids += $category->get_parents();
+ $categories = \coursecat::get_many($ids);
+ $courses = array_filter($courses, function($course) use ($categories) {
+ return array_search($course->category, $categories) !== false;
+ });
+ $category = $category->get_db_record();
+ }
}
- // TODO: Copy what we do in calendar/view.php.
- $context = \context_user::instance($USER->id);
- self::validate_context($context);
-
$type = \core_calendar\type_factory::get_calendar_instance();
$time = $type->convert_to_timestamp($year, $month, 1);
$calendar = new calendar_information(0, 0, 0, $time);
- $calendar->prepare_for_view($course, $courses);
+ $calendar->set_sources($course, $courses, $category);
list($data, $template) = calendar_get_view($calendar, 'month', $params['includenavigation']);
'year' => new external_value(PARAM_INT, 'Month to be viewed', VALUE_REQUIRED),
'month' => new external_value(PARAM_INT, 'Year to be viewed', VALUE_REQUIRED),
'courseid' => new external_value(PARAM_INT, 'Course being viewed', VALUE_DEFAULT, SITEID, NULL_ALLOWED),
+ 'categoryid' => new external_value(PARAM_INT, 'Category being viewed', VALUE_DEFAULT, null, NULL_ALLOWED),
'includenavigation' => new external_value(
PARAM_BOOL,
'Whether to show course navigation',
return \core_calendar\external\month_exporter::get_read_structure();
}
+ /**
+ * Get data for the daily calendar view.
+ *
+ * @param int $year The year to be shown
+ * @param int $month The month to be shown
+ * @param int $day The day to be shown
+ * @param int $courseid The course to be included
+ * @return array
+ */
+ public static function get_calendar_day_view($year, $month, $day, $courseid, $categoryid) {
+ global $CFG, $DB, $USER, $PAGE;
+ require_once($CFG->dirroot."/calendar/lib.php");
+
+ // Parameter validation.
+ $params = self::validate_parameters(self::get_calendar_day_view_parameters(), [
+ 'year' => $year,
+ 'month' => $month,
+ 'day' => $day,
+ 'courseid' => $courseid,
+ 'categoryid' => $categoryid,
+ ]);
+
+ if ($courseid != SITEID && !empty($courseid)) {
+ // Course ID must be valid and existing.
+ $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
+ $courses = [$course->id => $course];
+
+ $coursecat = \coursecat::get($course->category);
+ $category = $coursecat->get_db_record();
+ } else {
+ $course = get_site();
+ $courses = calendar_get_default_courses();
+ $category = null;
+
+ if ($categoryid) {
+ self::validate_context(context_coursecat::instance($categoryid));
+ $ids = [$categoryid];
+ $category = \coursecat::get($categoryid);
+ $ids += $category->get_parents();
+ $categories = \coursecat::get_many($ids);
+ $courses = array_filter($courses, function($course) use ($categories) {
+ return array_search($course->category, $categories) !== false;
+ });
+ $category = $category->get_db_record();
+ }
+ }
+
+ // TODO: Copy what we do in calendar/view.php.
+ $context = \context_user::instance($USER->id);
+ self::validate_context($context);
+
+ $type = \core_calendar\type_factory::get_calendar_instance();
+
+ $time = $type->convert_to_timestamp($year, $month, $day);
+ $calendar = new calendar_information(0, 0, 0, $time);
+ $calendar->set_sources($course, $courses, $category);
+
+ list($data, $template) = calendar_get_view($calendar, 'day', $params['includenavigation']);
+
+ return $data;
+ }
+
+ /**
+ * Returns description of method parameters.
+ *
+ * @return external_function_parameters
+ */
+ public static function get_calendar_day_view_parameters() {
+ return new external_function_parameters(
+ [
+ 'year' => new external_value(PARAM_INT, 'Year to be viewed', VALUE_REQUIRED),
+ 'month' => new external_value(PARAM_INT, 'Month to be viewed', VALUE_REQUIRED),
+ 'day' => new external_value(PARAM_INT, 'Day to be viewed', VALUE_REQUIRED),
+ 'courseid' => new external_value(PARAM_INT, 'Course being viewed', VALUE_DEFAULT, SITEID, NULL_ALLOWED),
+ 'categoryid' => new external_value(PARAM_INT, 'Category being viewed', VALUE_DEFAULT, null, NULL_ALLOWED),
+ ]
+ );
+ }
+
+ /**
+ * Returns description of method result value.
+ *
+ * @return external_description
+ */
+ public static function get_calendar_day_view_returns() {
+ return \core_calendar\external\calendar_day_exporter::get_read_structure();
+ }
+
+
/**
* Returns description of method parameters.
*
)
);
}
+
+ /**
+ * Get data for the monthly calendar view.
+ *
+ * @param int $courseid The course to be included
+ * @return array
+ */
+ public static function get_calendar_upcoming_view($courseid) {
+ global $CFG, $DB, $USER, $PAGE;
+ require_once($CFG->dirroot."/calendar/lib.php");
+
+ // Parameter validation.
+ self::validate_parameters(self::get_calendar_upcoming_view_parameters(), [
+ 'courseid' => $courseid,
+ ]);
+
+ if ($courseid != SITEID && !empty($courseid)) {
+ // Course ID must be valid and existing.
+ $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
+ $courses = [$course->id => $course];
+ } else {
+ $course = get_site();
+ $courses = calendar_get_default_courses();
+ }
+
+ $context = \context_user::instance($USER->id);
+ self::validate_context($context);
+
+ $calendar = new calendar_information(0, 0, 0, time());
+ $calendar->set_sources($course, $courses);
+
+ list($data, $template) = calendar_get_view($calendar, 'upcoming');
+
+ return $data;
+ }
+
+ /**
+ * Returns description of method parameters.
+ *
+ * @return external_function_parameters
+ */
+ public static function get_calendar_upcoming_view_parameters() {
+ return new external_function_parameters(
+ [
+ 'courseid' => new external_value(PARAM_INT, 'Course being viewed', VALUE_DEFAULT, SITEID, NULL_ALLOWED),
+ ]
+ );
+ }
+
+ /**
+ * Returns description of method result value.
+ *
+ * @return external_description
+ */
+ public static function get_calendar_upcoming_view_returns() {
+ return \core_calendar\external\calendar_upcoming_exporter::get_read_structure();
+ }
}
die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
}
+require_once($CFG->libdir . '/coursecatlib.php');
+
/**
* These are read by the administration component to provide default values
*/
*/
define('CALENDAR_EVENT_USER', 8);
+/**
+ * CALENDAR_EVENT_COURSECAT - Course category calendar event types
+ */
+define('CALENDAR_EVENT_COURSECAT', 16);
/**
* CALENDAR_IMPORT_FROM_FILE - import the calendar from a file
global $USER, $DB;
$context = null;
- if (isset($this->properties->courseid) && $this->properties->courseid > 0) {
+ if (isset($this->properties->categoryid) && $this->properties->categoryid > 0) {
+ $context = \context_coursecat::instance($this->properties->categoryid);
+ } else if (isset($this->properties->courseid) && $this->properties->courseid > 0) {
$context = \context_course::instance($this->properties->courseid);
} else if (isset($this->properties->course) && $this->properties->course > 0) {
$context = \context_course::instance($this->properties->course);
if ($this->editorcontext === null) {
// Switch on the event type to decide upon the appropriate context to use for this event.
$this->editorcontext = $this->properties->context;
- if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course'
- && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') {
+ if (!calendar_is_valid_eventtype($this->properties->eventtype)) {
return clean_text($this->properties->description, $this->properties->format);
}
}
$this->properties->groupid = 0;
$this->properties->userid = $USER->id;
break;
+ case 'category':
+ $this->properties->groupid = 0;
+ $this->properties->category = 0;
+ $this->properties->userid = $USER->id;
+ break;
case 'group':
$this->properties->userid = $USER->id;
break;
// We have a course and are within the course context so we had
// better use the courses max bytes value.
$this->editoroptions['maxbytes'] = $course->maxbytes;
+ } else if ($properties->eventtype === 'category') {
+ // First check the course is valid.
+ \coursecat::get($properties->categoryid, MUST_EXIST, true);
+ // Course context.
+ $this->editorcontext = $this->properties->context;
+ // We have a course and are within the course context so we had
+ // better use the courses max bytes value.
+ $this->editoroptions['maxbytes'] = $course->maxbytes;
} else {
// If we get here we have a custom event type as used by some
// modules. In this case the event will have been added by
// Switch on the event type to decide upon the appropriate context to use for this event.
$this->editorcontext = $this->properties->context;
- if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course'
- && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') {
+ if (!calendar_is_valid_eventtype($this->properties->eventtype)) {
// We don't have a context here, do a normal format_text.
return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id);
}
/** @var int A course id */
public $courseid = null;
+ /** @var array An array of categories */
+ public $categories = array();
+
+ /** @var int The current category */
+ public $categoryid = null;
+
/** @var array An array of courses */
public $courses = array();
* @return $this
*/
public function set_time($time = null) {
- if ($time === null) {
+ if (empty($time)) {
$this->time = time();
} else {
$this->time = $time;
/**
* Initialize calendar information
*
+ * @deprecated 3.4
* @param stdClass $course object
* @param array $coursestoload An array of courses [$course->id => $course]
* @param bool $ignorefilters options to use filter
*/
public function prepare_for_view(stdClass $course, array $coursestoload, $ignorefilters = false) {
- $this->courseid = $course->id;
+ debugging('The prepare_for_view() function has been deprecated. Please update your code to use set_sources()',
+ DEBUG_DEVELOPER);
+ $this->set_sources($course, $coursestoload);
+ }
+
+ /**
+ * Set the sources for events within the calendar.
+ *
+ * If no category is provided, then the category path for the current
+ * course will be used.
+ *
+ * @param stdClass $course The current course being viewed.
+ * @param int[] $courses The list of all courses currently accessible.
+ * @param stdClass $category The current category to show.
+ */
+ public function set_sources(stdClass $course, array $courses, stdClass $category = null) {
+ // A cousre must always be specified.
$this->course = $course;
- list($courses, $group, $user) = calendar_set_filters($coursestoload, $ignorefilters);
- $this->courses = $courses;
+ $this->courseid = $course->id;
+
+ list($courseids, $group, $user) = calendar_set_filters($courses);
+ $this->courses = $courseids;
$this->groups = $group;
$this->users = $user;
+
+ // Do not show category events by default.
+ $this->categoryid = null;
+ $this->categories = null;
+
+ if (null !== $category && $category->id > 0) {
+ // A specific category was requested - set the current category, and include all parents of that category.
+ $category = \coursecat::get($category->id);
+ $this->categoryid = $category->id;
+
+ $this->categories = $category->get_parents();
+ $this->categories[] = $category->id;
+ } else if (SITEID === $this->courseid) {
+ // This is the site.
+ // Show categories for all courses the user has access to.
+ $this->categories = true;
+ $categories = [];
+ foreach ($courses as $course) {
+ if ($category = \coursecat::get($course->category, IGNORE_MISSING)) {
+ $categories = array_merge($categories, $category->get_parents());
+ $categories[] = $category->id;
+ }
+ }
+
+ // And all categories that the user can manage.
+ foreach (\coursecat::get_all() as $category) {
+ if (!$category->has_manage_capability()) {
+ continue;
+ }
+
+ $categories = array_merge($categories, $category->get_parents());
+ $categories[] = $category->id;
+ }
+
+ $this->categories = array_unique($categories);
+ }
}
/**
* @param boolean $withduration whether only events starting within time range selected
* or events in progress/already started selected as well
* @param boolean $ignorehidden whether to select only visible events or all events
+ * @param array|int|boolean $categories array of categories, category id or boolean for all/no course events
* @return array $events of selected events or an empty array if there aren't any (or there was an error)
*/
-function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withduration=true, $ignorehidden=true) {
+function calendar_get_events($tstart, $tend, $users, $groups, $courses,
+ $withduration = true, $ignorehidden = true, $categories = []) {
global $DB;
$whereclause = '';
$params = array();
// Quick test.
- if (empty($users) && empty($groups) && empty($courses)) {
+ if (empty($users) && empty($groups) && empty($courses) && empty($categories)) {
return array();
}
// Events from a number of users
if(!empty($whereclause)) $whereclause .= ' OR';
list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED);
- $whereclause .= " (e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0)";
+ $whereclause .= " (e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)";
$params = array_merge($params, $inparamsusers);
} else if($users === true) {
// Events from ALL users
if(!empty($whereclause)) $whereclause .= ' OR';
- $whereclause .= ' (e.userid != 0 AND e.courseid = 0 AND e.groupid = 0)';
+ $whereclause .= ' (e.userid != 0 AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)';
} else if($users === false) {
// No user at all, do nothing
}
$whereclause .= ' (e.groupid = 0 AND e.courseid != 0)';
}
+ if ((is_array($categories) && !empty($categories)) || is_numeric($categories)) {
+ if (!empty($whereclause)) {
+ $whereclause .= ' OR';
+ }
+ list($insqlcategories, $inparamscategories) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED);
+ $whereclause .= " (e.groupid = 0 AND e.courseid = 0 AND e.categoryid $insqlcategories)";
+ $params = array_merge($params, $inparamscategories);
+ } else if ($categories === true) {
+ // Events from ALL categories.
+ if (!empty($whereclause)) {
+ $whereclause .= ' OR';
+ }
+ $whereclause .= ' (e.groupid = 0 AND e.courseid = 0 AND e.categoryid != 0)';
+ }
+
// Security check: if, by now, we have NOTHING in $whereclause, then it means
// that NO event-selecting clauses were defined. Thus, we won't be returning ANY
// events no matter what. Allowing the code to proceed might return a completely
(has_capability('moodle/calendar:managegroupentries', $event->context)
&& groups_is_member($event->groupid)));
} else if (!empty($event->courseid)) {
- // If groupid is not set, but course is set, it's definiely a course event.
+ // If groupid is not set, but course is set, it's definitely a course event.
+ return has_capability('moodle/calendar:manageentries', $event->context);
+ } else if (!empty($event->categoryid)) {
+ // If groupid is not set, but category is set, it's definitely a category event.
return has_capability('moodle/calendar:manageentries', $event->context);
} else if (!empty($event->userid) && $event->userid == $USER->id) {
// If course is not set, but userid id set, it's a user event.
*/
function calendar_set_event_type_display($type, $display = null, $user = null) {
$persist = get_user_preferences('calendar_persistflt', 0, $user);
- $default = CALENDAR_EVENT_GLOBAL + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER;
+ $default = CALENDAR_EVENT_GLOBAL + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP
+ + CALENDAR_EVENT_USER + CALENDAR_EVENT_COURSECAT;
if ($persist === 0) {
global $SESSION;
if (!isset($SESSION->calendarshoweventtype)) {
* @param stdClass $allowed list of allowed edit for event type
* @param stdClass|int $course object of a course or course id
* @param array $groups array of groups for the given course
+ * @param stdClass|int $category object of a category
*/
-function calendar_get_allowed_types(&$allowed, $course = null, $groups = null) {
+function calendar_get_allowed_types(&$allowed, $course = null, $groups = null, $category = null) {
global $USER, $DB;
$allowed = new \stdClass();
$allowed->user = has_capability('moodle/calendar:manageownentries', \context_system::instance());
$allowed->groups = false;
$allowed->courses = false;
+ $allowed->categories = false;
$allowed->site = has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID));
$getgroupsfunc = function($course, $context, $user) use ($groups) {
if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
}
}
}
+
+ if (!empty($category)) {
+ $catcontext = \context_coursecat::instance($category->id);
+ if (has_capability('moodle/category:manage', $catcontext)) {
+ $allowed->categories = [$category->id => 1];
+ }
+ }
}
/**
* The returned array will optionally have 5 keys:
* 'user' : true if the logged in user can create user events
* 'site' : true if the logged in user can create site events
+ * 'category' : array of course categories that the user can create events for
* 'course' : array of courses that the user can create events for
* 'group': array of groups that the user can create events for
* 'groupcourses' : array of courses that the groups belong to (can
* @return array The array of allowed types.
*/
function calendar_get_all_allowed_types() {
- global $CFG, $USER;
+ global $CFG, $USER, $DB;
require_once($CFG->libdir . '/enrollib.php');
$types['site'] = true;
}
+ if (coursecat::has_manage_capability_on_any()) {
+ $types['category'] = coursecat::make_categories_list('moodle/category:manage');
+ }
+
// This function warms the context cache for the course so the calls
// to load the course context in calendar_get_allowed_types don't result
// in additional DB queries.
calendar_get_allowed_types($allowed, $course);
- return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->site);
+ return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->category || $allowed->site);
}
/**
}
switch ($event->eventtype) {
+ case 'category':
+ return has_capability('moodle/category:manage', $event->context);
case 'course':
return has_capability('moodle/calendar:manageentries', $event->context);
case 'group':
if (!empty($allowed->courses)) {
$choices['course'] = get_string('courseevents', 'calendar');
}
+ if (!empty($allowed->categories)) {
+ $choices['category'] = get_string('categoryevents', 'calendar');
+ }
if (!empty($allowed->groups) and is_array($allowed->groups)) {
$choices['group'] = get_string('group');
}
$sub->courseid = $SITE->id;
} else if ($sub->eventtype === 'group' || $sub->eventtype === 'course') {
$sub->courseid = $sub->course;
+ } else if ($sub->eventtype === 'category') {
+ $sub->categoryid = $sub->category;
} else {
// User events.
$sub->courseid = 0;
* @param boolean $ignorehidden whether to select only visible events or all events
* @return array $events of selected events or an empty array if there aren't any (or there was an error)
*/
-function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $withduration = true, $ignorehidden = true) {
+function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses,
+ $withduration = true, $ignorehidden = true, $categories = []) {
// Normalise the users, groups and courses parameters so that they are compliant with \core_calendar\local\api::get_events().
// Existing functions that were using the old calendar_get_events() were passing a mixture of array, int, boolean for these
// parameters, but with the new API method, only null and arrays are accepted.
- list($userparam, $groupparam, $courseparam) = array_map(function($param) {
+ list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) {
// If parameter is true, return null.
if ($param === true) {
return null;
// No normalisation required.
return $param;
- }, [$users, $groups, $courses]);
+ }, [$users, $groups, $courses, $categories]);
$mapper = \core_calendar\local\event\container::get_event_mapper();
$events = \core_calendar\local\api::get_events(
$userparam,
$groupparam,
$courseparam,
+ $categoryparam,
$withduration,
$ignorehidden
);
} else {
$defaultlookahead = CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD;
}
- $tstart = $type->convert_to_timestamp($date['year'], $date['mon'], 1);
- $tend = $tstart + get_user_preferences('calendar_lookahead', $defaultlookahead);
+
+ $tstart = $type->convert_to_timestamp($date['year'], $date['mon'], $date['mday'], $date['hours']);
+ $tend = usergetmidnight($tstart + DAYSECS * $defaultlookahead + 3 * HOURSECS) - 1;
} else {
$tstart = $type->convert_to_timestamp($date['year'], $date['mon'], 1);
$monthdays = $type->get_num_days_in_month($date['year'], $date['mon']);
}
}
- list($userparam, $groupparam, $courseparam) = array_map(function($param) {
+ list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) {
// If parameter is true, return null.
if ($param === true) {
return null;
// No normalisation required.
return $param;
- }, [$calendar->users, $calendar->groups, $calendar->courses]);
+ }, [$calendar->users, $calendar->groups, $calendar->courses, $calendar->categories]);
$events = \core_calendar\local\api::get_events(
$tstart,
$userparam,
$groupparam,
$courseparam,
+ $categoryparam,
true,
true,
function ($event) {
if ($proxy = $event->get_course_module()) {
$cminfo = $proxy->get_proxied_instance();
return $cminfo->uservisible;
+ }
+
+ if ($proxy = $event->get_category()) {
+ $category = $proxy->get_proxied_instance();
+ return $category->is_uservisible();
}
return true;
$month->set_includenavigation($includenavigation);
$data = $month->export($renderer);
} else if ($view == "day") {
- $daydata = $type->timestamp_to_date_array($tstart);
- $day = new \core_calendar\external\day_exporter($calendar, $daydata, $related);
+ $day = new \core_calendar\external\calendar_day_exporter($calendar, $related);
$data = $day->export($renderer);
- $template = 'core_calendar/day_detailed';
+ $template = 'core_calendar/calendar_day';
+ } else if ($view == "upcoming") {
+ $upcoming = new \core_calendar\external\calendar_upcoming_exporter($calendar, $related);
+ $data = $upcoming->export($renderer);
+ $template = 'core_calendar/calendar_upcoming';
}
return [$data, $template];
$eventid = isset($args['eventid']) ? clean_param($args['eventid'], PARAM_INT) : null;
$starttime = isset($args['starttime']) ? clean_param($args['starttime'], PARAM_INT) : null;
$courseid = isset($args['courseid']) ? clean_param($args['courseid'], PARAM_INT) : null;
+ $categoryid = isset($args['categoryid']) ? clean_param($args['categoryid'], PARAM_INT) : null;
$event = null;
$hasformdata = isset($args['formdata']) && !empty($args['formdata']);
$context = \context_user::instance($USER->id);
$data['eventtype'] = 'course';
$data['courseid'] = $courseid;
$data['groupcourseid'] = $courseid;
+ } else if (!empty($categoryid)) {
+ $data['eventtype'] = 'category';
+ $data['categoryid'] = $categoryid;
}
$mform->set_data($data);
} else {
function calendar_get_filter_types() {
$types = [
'site',
+ 'category',
'course',
'group',
'user',
];
}, $types);
}
+
+/**
+ * Check whether the specified event type is valid.
+ *
+ * @param string $type
+ * @return bool
+ */
+function calendar_is_valid_eventtype($type) {
+ $validtypes = [
+ 'user',
+ 'group',
+ 'course',
+ 'category',
+ 'site',
+ ];
+ return in_array($type, $validtypes);
+}
return html_writer::tag('div', $eventhtml, array('class' => 'event', 'id' => 'event_' . $event->id));
}
- /**
- * Displays upcoming events
- *
- * @param calendar_information $calendar
- * @param int $futuredays
- * @param int $maxevents
- * @return string
- */
- public function show_upcoming_events(calendar_information $calendar, $futuredays, $maxevents, moodle_url $returnurl = null) {
-
- if ($returnurl === null) {
- $returnurl = $this->page->url;
- }
-
- $events = calendar_get_upcoming($calendar->courses, $calendar->groups, $calendar->users,
- $futuredays, $maxevents);
-
- $output = html_writer::start_tag('div', array('class'=>'header'));
- $output .= $this->course_filter_selector($returnurl, get_string('upcomingeventsfor', 'calendar'));
- if (calendar_user_can_add_event($calendar->course)) {
- $output .= $this->add_event_button($calendar->course->id);
- }
- $output .= html_writer::end_tag('div');
-
- if ($events) {
- $output .= html_writer::start_tag('div', array('class' => 'eventlist'));
- foreach ($events as $event) {
- // Convert to calendar_event object so that we transform description accordingly.
- $event = new calendar_event($event);
- $event->calendarcourseid = $calendar->courseid;
- $output .= $this->event($event);
- }
- $output .= html_writer::end_tag('div');
- } else {
- $output .= html_writer::span(get_string('noupcomingevents', 'calendar'), 'calendar-information calendar-no-results');
- }
-
- return $output;
- }
-
/**
* Displays a course filter selector
*
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template calendar/calendar_day
+
+ Calendar day view.
+
+ The purpose of this template is to render the calendar day view.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Example context (json):
+ {
+ }
+}}
+<div id="calendar-day-{{uniqid}}" data-template="core_calendar/day_detailed">
+ {{> core_calendar/day_detailed}}
+</div>
+{{#js}}
+require(['jquery', 'core_calendar/calendar_view'], function($, CalendarView) {
+ CalendarView.init($("#calendar-day-{{uniqid}}"), 'day');
+});
+{{/js}}
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template calendar/calendar_upcoming
+
+ Calendar upcoming view.
+
+ The purpose of this template is to render the calendar upcoming view.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Example context (json):
+ {
+ }
+}}
+<div id="calendar-upcoming-{{uniqid}}" data-template="core_calendar/upcoming_detailed">
+ {{> core_calendar/upcoming_detailed}}
+</div>
+{{#js}}
+require(['jquery', 'core_calendar/calendar_view'], function($, CalendarView) {
+ CalendarView.init($("#calendar-upcoming-{{uniqid}}"), 'upcoming');
+});
+{{/js}}
{
}
}}
-<div class="header">
- {{{filter_selector}}}
- {{{new_event_button}}}
+<div{{!
+ }} class="calendarwrapper"{{!
+ }} data-context-id="{{defaulteventcontext}}"{{!
+ }}{{#courseid}} data-courseid="{{courseid}}"{{/courseid}}{{!
+ }}{{#categoryid}} data-categoryid="{{categoryid}}"{{/categoryid}}{{!
+ }} data-view="day"{{!
+ }} data-year="{{date.year}}"{{!
+ }} data-month="{{date.mon}}"{{!
+ }} data-day="{{date.mday}}"{{!
+ }} data-region="day"{{!
+ }} data-new-event-timestamp="{{neweventtimestamp}}"{{!
+}}>
+ {{> core_calendar/header}}
+ {{> core_calendar/day_navigation }}
+ {{> core/overlay_loading}}
+ {{> core_calendar/event_list }}
</div>
-{{> core_calendar/day_navigation }}
-{{> core_calendar/event_list }}
\ No newline at end of file
{
}
}}
-{{#navigation}}
-<div class="controls" data-view="{{view}}">
- {{{navigation}}}
+<div id="day-navigation-{{uniqid}}" class="controls clearfix">
+ <div class="calendar-controls">
+ <a{{!
+ }} href="{{previousperiodlink}}"{{!
+ }} class="arrow_link previous"{{!
+ }} title="{{#str}}dayprev, calendar{{/str}}"{{!
+ }} data-year="{{previousperiod.year}}"{{!
+ }} data-month="{{previousperiod.mon}}"{{!
+ }} data-day="{{previousperiod.mday}}"{{!
+ }}>
+ <span class="arrow">{{{larrow}}}</span>
+
+ <span class="arrow_text">{{previousperiodname}}</span>
+ </a>
+ <span class="hide"> | </span>
+ <h2 class="current">{{periodname}}</h2>
+ <span class="hide"> | </span>
+ <a{{!
+ }} href="{{nextperiodlink}}"{{!
+ }} class="arrow_link next"{{!
+ }} title="{{#str}}daynext, calendar{{/str}}"{{!
+ }} data-year="{{nextperiod.year}}"{{!
+ }} data-month="{{nextperiod.mon}}"{{!
+ }} data-day="{{nextperiod.mday}}"{{!
+ }}>
+ <span class="arrow_text">{{nextperiodname}}</span>
+
+ <span class="arrow">{{{rarrow}}}</span>
+ </a>
+ </div>
</div>
-{{/navigation}}
-
{
}
}}
-<div class="event">
+<div{{!
+ }} data-type="event"{{!
+ }} data-course-id="{{course.id}}"{{!
+ }} data-event-id="{{id}}"{{!
+ }} class="event"{{!
+ }} data-eventtype-{{calendareventtype}}="1"{{!
+ }} data-event-title="{{name}}"{{!
+ }} data-event-count="{{eventcount}}"{{!
+ }}>
<div class="card">
<div class="box card-header clearfix p-y-1">
<div class="commands pull-xs-right">
{{#canedit}}
{{#candelete}}
- <a href="{{deleteurl}}">
+ <a href="{{deleteurl}}" data-action="delete">
{{#pix}}t/delete, core, {{#str}}delete{{/str}}{{/pix}}
</a>
{{/candelete}}
- <a href="{{editurl}}">
+ <a href="{{editurl}}" data-action="edit">
{{#pix}}t/edit, core, {{#str}}edit{{/str}}{{/pix}}
</a>
{{/canedit}}
{{/groupname}}
</div>
</div>
-</div>
\ No newline at end of file
+</div>
{
}
}}
-<div class="eventlist">
+<div class="eventlist m-y-1">
{{#events}}
{{> core_calendar/event_item }}
{{/events}}
}} data-region="summary-modal-container"{{!
}} data-event-id="{{id}}"{{!
}} data-event-title="{{name}}"{{!
- }} data-event-event-count="{{eventcount}}"{{!
+ }} data-event-count="{{eventcount}}"{{!
}} data-event-="{{repeatid}}"{{!
}} data-action-event="{{isactionevent}}"{{!
}} data-edit-url="{{editurl}}"{{!
{{/description}}
<h4>{{#str}} eventtype, core_calendar {{/str}}</h4>
{{eventtype}}
+ {{#iscategoryevent}}
+ <div>{{{category.nestedname}}}</div>
+ {{/iscategoryevent}}
{{#iscourseevent}}
<div><a href="{{url}}">{{course.fullname}}</a></div>
{{/iscourseevent}}
Example context (json):
{
"exportcalendarbutton": "<button class='btn btn-secondary'>Export calendar</button>",
- "managesubscriptionbutton": "<button class='btn btn-secondary'>Manage subscriptions</button>",
- "icalurl": "http://example.com/"
+ "managesubscriptionbutton": "<button class='btn btn-secondary'>Manage subscriptions</button>"
}
}}
<div class="bottom">
{{#managesubscriptionbutton}}
{{> core/single_button }}
{{/managesubscriptionbutton}}
- {{#icalurl}}
- <a href="{{icalurl}}" title="{{#str}} quickdownloadcalendar, calendar {{/str}}" class="ical-link m-l-1">iCal</a>
- {{/icalurl}}
</div>
{
}
}}
-{{#filter_selector}}
<div class="header">
- {{{filter_selector}}}
+ {{#filter_selector}}
+ {{{filter_selector}}}
+ {{/filter_selector}}
+ {{> core_calendar/add_event_button}}
</div>
-{{/filter_selector}}
}}
<div{{!
}} class="calendarwrapper"{{!
- }} data-courseid="{{courseid}}"{{!
+ }}{{#courseid}} data-courseid="{{courseid}}"{{/courseid}}{{!
+ }}{{#categoryid}} data-categoryid="{{categoryid}}"{{/categoryid}}{{!
+ }} data-context-id="{{defaulteventcontext}}"{{!
}} data-month="{{date.mon}}"{{!
}} data-year="{{date.year}}"{{!
+ }} data-view="month"{{!
}}>
{{> core_calendar/header }}
- {{> core_calendar/add_event_button}}
{{> core_calendar/month_navigation }}
{{> core/overlay_loading}}
<table id="month-detailed-{{uniqid}}" class="calendarmonth calendartable card-deck m-b-0">
}} id="month-mini-{{date.year}}-{{date.month}}-{{uniqid}}"{{!
}} class="calendarwrapper"{{!
}} data-courseid="{{courseid}}"{{!
+ }} data-categoryid="{{categoryid}}"{{!
}} data-month="{{date.mon}}"{{!