ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PK*\_llexternallib.phpnu[. /** * External calendar API * * @package core_calendar * @category external * @copyright 2012 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.5 */ defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot . '/calendar/lib.php'); use core_calendar\local\api as local_api; use core_calendar\local\event\container as event_container; use core_calendar\local\event\forms\create as create_event_form; use core_calendar\local\event\forms\update as update_event_form; use core_calendar\local\event\mappers\create_update_form_mapper; use core_calendar\external\event_exporter; use core_calendar\external\events_exporter; use core_calendar\external\events_grouped_by_course_exporter; use core_calendar\external\events_related_objects_cache; use core_external\external_api; use core_external\external_format_value; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\util as external_util; /** * Calendar external functions * * @package core_calendar * @category external * @copyright 2012 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.5 */ class core_calendar_external extends external_api { /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.5 */ public static function delete_calendar_events_parameters() { return new external_function_parameters( array('events' => new external_multiple_structure( new external_single_structure( array( 'eventid' => new external_value(PARAM_INT, 'Event ID', VALUE_REQUIRED, '', NULL_NOT_ALLOWED), 'repeat' => new external_value(PARAM_BOOL, 'Delete comeplete series if repeated event') ), 'List of events to delete' ) ) ) ); } /** * Delete Calendar events * * @param array $eventids A list of event ids with repeat flag to delete * @return null * @since Moodle 2.5 */ public static function delete_calendar_events($events) { global $DB; // Parameter validation. $params = self::validate_parameters(self:: delete_calendar_events_parameters(), array('events' => $events)); $transaction = $DB->start_delegated_transaction(); foreach ($params['events'] as $event) { $eventobj = calendar_event::load($event['eventid']); // Let's check if the user is allowed to delete an event. if (!calendar_delete_event_allowed($eventobj)) { throw new moodle_exception('nopermissions', 'error', '', get_string('deleteevent', 'calendar')); } // Time to do the magic. $eventobj->delete($event['repeat']); } // Everything done smoothly, let's commit. $transaction->allow_commit(); return null; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.5 */ public static function delete_calendar_events_returns() { return null; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.5 */ public static function get_calendar_events_parameters() { return new external_function_parameters( array('events' => new external_single_structure( array( 'eventids' => new external_multiple_structure( new external_value(PARAM_INT, 'event ids') , 'List of event ids', VALUE_DEFAULT, array()), 'courseids' => new external_multiple_structure( new external_value(PARAM_INT, 'course ids') , 'List of course ids for which events will be returned', VALUE_DEFAULT, array()), 'groupids' => new external_multiple_structure( new external_value(PARAM_INT, 'group ids') , 'List of group ids for which events should be returned', VALUE_DEFAULT, array()), 'categoryids' => new external_multiple_structure( new external_value(PARAM_INT, 'Category ids'), 'List of category ids for which events will be returned', VALUE_DEFAULT, array()), ), 'Event details', VALUE_DEFAULT, array()), 'options' => new external_single_structure( array( 'userevents' => new external_value(PARAM_BOOL, "Set to true to return current user's user events", VALUE_DEFAULT, true, NULL_ALLOWED), 'siteevents' => new external_value(PARAM_BOOL, "Set to true to return site events", VALUE_DEFAULT, true, NULL_ALLOWED), 'timestart' => new external_value(PARAM_INT, "Time from which events should be returned", VALUE_DEFAULT, 0, NULL_ALLOWED), 'timeend' => new external_value(PARAM_INT, "Time to which the events should be returned. We treat 0 and null as no end", VALUE_DEFAULT, 0, NULL_ALLOWED), 'ignorehidden' => new external_value(PARAM_BOOL, "Ignore hidden events or not", VALUE_DEFAULT, true, NULL_ALLOWED), ), 'Options', VALUE_DEFAULT, array()) ) ); } /** * Get Calendar events * * @param array $events A list of events * @param array $options various options * @return array Array of event details * @since Moodle 2.5 */ public static function get_calendar_events($events = array(), $options = array()) { global $SITE, $DB, $USER; // Parameter validation. $params = self::validate_parameters(self::get_calendar_events_parameters(), array('events' => $events, 'options' => $options)); $funcparam = array('courses' => array(), 'groups' => array(), 'categories' => array()); $hassystemcap = has_capability('moodle/calendar:manageentries', context_system::instance()); $warnings = array(); $coursecategories = array(); // Let us find out courses and their categories that we can return events from. if (!$hassystemcap) { $courseobjs = enrol_get_my_courses(); $courses = array_keys($courseobjs); $coursecategories = array_flip(array_map(function($course) { return $course->category; }, $courseobjs)); foreach ($params['events']['courseids'] as $id) { try { $context = context_course::instance($id); self::validate_context($context); $funcparam['courses'][] = $id; } catch (Exception $e) { $warnings[] = array( 'item' => 'course', 'itemid' => $id, 'warningcode' => 'nopermissions', 'message' => 'No access rights in course context '.$e->getMessage().$e->getTraceAsString() ); } } } else { $courses = $params['events']['courseids']; $funcparam['courses'] = $courses; if (!empty($courses)) { list($wheresql, $sqlparams) = $DB->get_in_or_equal($courses); $wheresql = "id $wheresql"; $coursecategories = array_flip(array_map(function($course) { return $course->category; }, $DB->get_records_select('course', $wheresql, $sqlparams, '', 'category'))); } } // Let us findout groups that we can return events from. if (!$hassystemcap) { $groups = groups_get_my_groups(); $groups = array_keys($groups); foreach ($params['events']['groupids'] as $id) { if (in_array($id, $groups)) { $funcparam['groups'][] = $id; } else { $warnings[] = array('item' => $id, 'warningcode' => 'nopermissions', 'message' => 'you do not have permissions to access this group'); } } } else { $groups = $params['events']['groupids']; $funcparam['groups'] = $groups; } $categories = array(); if ($hassystemcap || !empty($courses)) { // Use the category id as the key in the following array. That way we do not have to remove duplicates and // have a faster lookup later. $categories = []; if (!empty($params['events']['categoryids'])) { $catobjs = \core_course_category::get_many( array_merge($params['events']['categoryids'], array_keys($coursecategories))); foreach ($catobjs as $catobj) { if (isset($coursecategories[$catobj->id]) || has_capability('moodle/category:manage', $catobj->get_context())) { // If the user has access to a course in this category or can manage the category, // then they can see all parent categories too. $categories[$catobj->id] = true; foreach ($catobj->get_parents() as $catid) { $categories[$catid] = true; } } } $funcparam['categories'] = array_keys($categories); } else { // Fetch all categories where this user has any enrolment, and all categories that this user can manage. $calcatcache = cache::make('core', 'calendar_categories'); // Do not use cache if the user has the system capability as $coursecategories might not represent the // courses the user is enrolled in. $categories = (!$hassystemcap) ? $calcatcache->get('site') : false; if ($categories !== false) { // The ids are stored in a list in the cache. $funcparam['categories'] = $categories; $categories = array_flip($categories); } else { $categories = []; foreach (\core_course_category::get_all() as $category) { if (isset($coursecategories[$category->id]) || has_capability('moodle/category:manage', $category->get_context(), $USER, false)) { // If the user has access to a course in this category or can manage the category, // then they can see all parent categories too. $categories[$category->id] = true; foreach ($category->get_parents() as $catid) { $categories[$catid] = true; } } } $funcparam['categories'] = array_keys($categories); if (!$hassystemcap) { $calcatcache->set('site', $funcparam['categories']); } } } } // Do we need user events? if (!empty($params['options']['userevents'])) { $funcparam['users'] = array($USER->id); } else { $funcparam['users'] = false; } // Do we need site events? if (!empty($params['options']['siteevents'])) { $funcparam['courses'][] = $SITE->id; } // We treat 0 and null as no end. if (empty($params['options']['timeend'])) { $params['options']['timeend'] = PHP_INT_MAX; } // Event list does not check visibility and permissions, we'll check that later. $eventlist = calendar_get_legacy_events($params['options']['timestart'], $params['options']['timeend'], $funcparam['users'], $funcparam['groups'], $funcparam['courses'], true, $params['options']['ignorehidden'], $funcparam['categories']); // WS expects arrays. $events = array(); // We need to get events asked for eventids. if ($eventsbyid = calendar_get_events_by_id($params['events']['eventids'])) { $eventlist += $eventsbyid; } foreach ($eventlist as $eventid => $eventobj) { $event = (array) $eventobj; // Description formatting. $calendareventobj = new calendar_event($event); $event['name'] = $calendareventobj->format_external_name(); list($event['description'], $event['format']) = $calendareventobj->format_external_text(); if ($hassystemcap) { // User can see everything, no further check is needed. $events[$eventid] = $event; } else if (!empty($eventobj->modulename)) { $courseid = $eventobj->courseid; if (!$courseid) { if (!$calendareventobj->context || !($context = $calendareventobj->context->get_course_context(false))) { continue; } $courseid = $context->instanceid; } $instances = get_fast_modinfo($courseid)->get_instances_of($eventobj->modulename); if (!empty($instances[$eventobj->instance]->uservisible)) { $events[$eventid] = $event; } } else { // Can the user actually see this event? $eventobj = calendar_event::load($eventobj); if ((($eventobj->courseid == $SITE->id) && (empty($eventobj->categoryid))) || (!empty($eventobj->categoryid) && isset($categories[$eventobj->categoryid])) || (!empty($eventobj->groupid) && in_array($eventobj->groupid, $groups)) || (!empty($eventobj->courseid) && in_array($eventobj->courseid, $courses)) || ($USER->id == $eventobj->userid) || (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'); } } } return array('events' => $events, 'warnings' => $warnings); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.5 */ public static function get_calendar_events_returns() { return new external_single_structure(array( 'events' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'event id'), 'name' => new external_value(PARAM_RAW, 'event name'), 'description' => new external_value(PARAM_RAW, 'Description', VALUE_OPTIONAL, null, NULL_ALLOWED), 'format' => new external_format_value('description'), 'courseid' => new external_value(PARAM_INT, 'course id'), 'categoryid' => new external_value(PARAM_INT, 'Category id (only for category events).', VALUE_OPTIONAL), 'groupid' => new external_value(PARAM_INT, 'group id'), 'userid' => new external_value(PARAM_INT, 'user id'), 'repeatid' => new external_value(PARAM_INT, 'repeat id'), 'modulename' => new external_value(PARAM_TEXT, 'module name', VALUE_OPTIONAL, null, NULL_ALLOWED), 'instance' => new external_value(PARAM_INT, 'instance id'), 'eventtype' => new external_value(PARAM_TEXT, 'Event type'), 'timestart' => new external_value(PARAM_INT, 'timestart'), 'timeduration' => new external_value(PARAM_INT, 'time duration'), 'visible' => new external_value(PARAM_INT, 'visible'), 'uuid' => new external_value(PARAM_TEXT, 'unique id of ical events', VALUE_OPTIONAL, null, NULL_NOT_ALLOWED), 'sequence' => new external_value(PARAM_INT, 'sequence'), 'timemodified' => new external_value(PARAM_INT, 'time modified'), 'subscriptionid' => new external_value(PARAM_INT, 'Subscription id', VALUE_OPTIONAL, null, NULL_ALLOWED), ), 'event') ), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters. * * @since Moodle 3.3 * @return external_function_parameters */ public static function get_calendar_action_events_by_timesort_parameters() { return new external_function_parameters( array( 'timesortfrom' => new external_value(PARAM_INT, 'Time sort from', VALUE_DEFAULT, 0), 'timesortto' => new external_value(PARAM_INT, 'Time sort to', VALUE_DEFAULT, null), 'aftereventid' => new external_value(PARAM_INT, 'The last seen event id', VALUE_DEFAULT, 0), 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 20), 'limittononsuspendedevents' => new external_value(PARAM_BOOL, 'Limit the events to courses the user is not suspended in', VALUE_DEFAULT, false), 'userid' => new external_value(PARAM_INT, 'The user id', VALUE_DEFAULT, null), 'searchvalue' => new external_value(PARAM_RAW, 'The value a user wishes to search against', VALUE_DEFAULT, null) ) ); } /** * Get calendar action events based on the timesort value. * * @since Moodle 3.3 * @param null|int $timesortfrom Events after this time (inclusive) * @param null|int $timesortto Events before this time (inclusive) * @param null|int $aftereventid Get events with ids greater than this one * @param int $limitnum Limit the number of results to this value * @param null|int $userid The user id * @param string|null $searchvalue The value a user wishes to search against * @return array */ public static function get_calendar_action_events_by_timesort($timesortfrom = 0, $timesortto = null, $aftereventid = 0, $limitnum = 20, $limittononsuspendedevents = false, $userid = null, ?string $searchvalue = null) { global $PAGE, $USER; $params = self::validate_parameters( self::get_calendar_action_events_by_timesort_parameters(), [ 'timesortfrom' => $timesortfrom, 'timesortto' => $timesortto, 'aftereventid' => $aftereventid, 'limitnum' => $limitnum, 'limittononsuspendedevents' => $limittononsuspendedevents, 'userid' => $userid, 'searchvalue' => $searchvalue ] ); if ($params['userid']) { $user = \core_user::get_user($params['userid']); } else { $user = $USER; } $context = \context_user::instance($user->id); self::validate_context($context); if ($params['userid'] && $USER->id !== $params['userid'] && !has_capability('moodle/calendar:manageentries', $context)) { throw new \required_capability_exception($context, 'moodle/calendar:manageentries', 'nopermission', ''); } if (empty($params['aftereventid'])) { $params['aftereventid'] = null; } $renderer = $PAGE->get_renderer('core_calendar'); $events = local_api::get_action_events_by_timesort( $params['timesortfrom'], $params['timesortto'], $params['aftereventid'], $params['limitnum'], $params['limittononsuspendedevents'], $user, clean_param($params['searchvalue'], PARAM_TEXT) ); $exportercache = new events_related_objects_cache($events); $exporter = new events_exporter($events, ['cache' => $exportercache]); return $exporter->export($renderer); } /** * Returns description of method result value. * * @since Moodle 3.3 * @return \core_external\external_description */ public static function get_calendar_action_events_by_timesort_returns() { return events_exporter::get_read_structure(); } /** * Returns description of method parameters. * * @return external_function_parameters */ public static function get_calendar_action_events_by_course_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'Course id'), 'timesortfrom' => new external_value(PARAM_INT, 'Time sort from', VALUE_DEFAULT, null), 'timesortto' => new external_value(PARAM_INT, 'Time sort to', VALUE_DEFAULT, null), 'aftereventid' => new external_value(PARAM_INT, 'The last seen event id', VALUE_DEFAULT, 0), 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 20), 'searchvalue' => new external_value(PARAM_RAW, 'The value a user wishes to search against', VALUE_DEFAULT, null) ) ); } /** * Get calendar action events for the given course. * * @since Moodle 3.3 * @param int $courseid Only events in this course * @param null|int $timesortfrom Events after this time (inclusive) * @param null|int $timesortto Events before this time (inclusive) * @param null|int $aftereventid Get events with ids greater than this one * @param int $limitnum Limit the number of results to this value * @param string|null $searchvalue The value a user wishes to search against * @return array */ public static function get_calendar_action_events_by_course( $courseid, $timesortfrom = null, $timesortto = null, $aftereventid = 0, $limitnum = 20, ?string $searchvalue = null) { global $PAGE, $USER; $user = null; $params = self::validate_parameters( self::get_calendar_action_events_by_course_parameters(), [ 'courseid' => $courseid, 'timesortfrom' => $timesortfrom, 'timesortto' => $timesortto, 'aftereventid' => $aftereventid, 'limitnum' => $limitnum, 'searchvalue' => $searchvalue ] ); $context = \context_user::instance($USER->id); self::validate_context($context); if (empty($params['aftereventid'])) { $params['aftereventid'] = null; } $courses = enrol_get_my_courses('*', null, 0, [$courseid]); $courses = array_values($courses); if (empty($courses)) { return []; } $course = $courses[0]; $renderer = $PAGE->get_renderer('core_calendar'); $events = local_api::get_action_events_by_course( $course, $params['timesortfrom'], $params['timesortto'], $params['aftereventid'], $params['limitnum'], clean_param($params['searchvalue'], PARAM_TEXT) ); $exportercache = new events_related_objects_cache($events, $courses); $exporter = new events_exporter($events, ['cache' => $exportercache]); return $exporter->export($renderer); } /** * Returns description of method result value. * * @return \core_external\external_description */ public static function get_calendar_action_events_by_course_returns() { return events_exporter::get_read_structure(); } /** * Returns description of method parameters. * * @return external_function_parameters */ public static function get_calendar_action_events_by_courses_parameters() { return new external_function_parameters( array( 'courseids' => new external_multiple_structure( new external_value(PARAM_INT, 'Course id') ), 'timesortfrom' => new external_value(PARAM_INT, 'Time sort from', VALUE_DEFAULT, null), 'timesortto' => new external_value(PARAM_INT, 'Time sort to', VALUE_DEFAULT, null), 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 10), 'searchvalue' => new external_value(PARAM_RAW, 'The value a user wishes to search against', VALUE_DEFAULT, null) ) ); } /** * Get calendar action events for a given list of courses. * * @since Moodle 3.3 * @param array $courseids Only include events for these courses * @param null|int $timesortfrom Events after this time (inclusive) * @param null|int $timesortto Events before this time (inclusive) * @param int $limitnum Limit the number of results per course to this value * @param string|null $searchvalue The value a user wishes to search against * @return array */ public static function get_calendar_action_events_by_courses( array $courseids, $timesortfrom = null, $timesortto = null, $limitnum = 10, ?string $searchvalue = null) { global $PAGE, $USER; $user = null; $params = self::validate_parameters( self::get_calendar_action_events_by_courses_parameters(), [ 'courseids' => $courseids, 'timesortfrom' => $timesortfrom, 'timesortto' => $timesortto, 'limitnum' => $limitnum, 'searchvalue' => $searchvalue ] ); $context = \context_user::instance($USER->id); self::validate_context($context); if (empty($params['courseids'])) { return ['groupedbycourse' => []]; } $renderer = $PAGE->get_renderer('core_calendar'); $courses = enrol_get_my_courses('*', null, 0, $params['courseids']); $courses = array_values($courses); if (empty($courses)) { return ['groupedbycourse' => []]; } $events = local_api::get_action_events_by_courses( $courses, $params['timesortfrom'], $params['timesortto'], $params['limitnum'], clean_param($params['searchvalue'], PARAM_TEXT) ); if (empty($events)) { return ['groupedbycourse' => []]; } $exportercache = new events_related_objects_cache($events, $courses); $exporter = new events_grouped_by_course_exporter($events, ['cache' => $exportercache]); return $exporter->export($renderer); } /** * Returns description of method result value. * * @return \core_external\external_description */ public static function get_calendar_action_events_by_courses_returns() { return events_grouped_by_course_exporter::get_read_structure(); } /** * Returns description of method parameters. * * @return external_function_parameters. * @since Moodle 2.5 */ public static function create_calendar_events_parameters() { // Userid is always current user, so no need to get it from client. // Module based calendar events are not allowed here. Hence no need of instance and modulename. // subscription id and uuid is not allowed as this is not an ical api. return new external_function_parameters( array('events' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_TEXT, 'event name', VALUE_REQUIRED, '', NULL_NOT_ALLOWED), 'description' => new external_value(PARAM_RAW, 'Description', VALUE_DEFAULT, null, NULL_ALLOWED), 'format' => new external_format_value('description', VALUE_DEFAULT), 'courseid' => new external_value(PARAM_INT, 'course id', VALUE_DEFAULT, 0, NULL_NOT_ALLOWED), 'groupid' => new external_value(PARAM_INT, 'group id', VALUE_DEFAULT, 0, NULL_NOT_ALLOWED), 'repeats' => new external_value(PARAM_INT, 'number of repeats', VALUE_DEFAULT, 0, NULL_NOT_ALLOWED), 'eventtype' => new external_value(PARAM_TEXT, 'Event type', VALUE_DEFAULT, 'user', NULL_NOT_ALLOWED), 'timestart' => new external_value(PARAM_INT, 'timestart', VALUE_DEFAULT, time(), NULL_NOT_ALLOWED), 'timeduration' => new external_value(PARAM_INT, 'time duration', VALUE_DEFAULT, 0, NULL_NOT_ALLOWED), 'visible' => new external_value(PARAM_INT, 'visible', VALUE_DEFAULT, 1, NULL_NOT_ALLOWED), 'sequence' => new external_value(PARAM_INT, 'sequence', VALUE_DEFAULT, 1, NULL_NOT_ALLOWED), ), 'event') ) ) ); } /** * Create calendar events. * * @param array $events A list of events to create. * @return array array of events created. * @since Moodle 2.5 * @throws moodle_exception if user doesnt have the permission to create events. */ public static function create_calendar_events($events) { global $DB, $USER; // Parameter validation. $params = self::validate_parameters(self::create_calendar_events_parameters(), array('events' => $events)); $transaction = $DB->start_delegated_transaction(); $return = array(); $warnings = array(); foreach ($params['events'] as $event) { // Let us set some defaults. $event['userid'] = $USER->id; $event['modulename'] = ''; $event['instance'] = 0; $event['subscriptionid'] = null; $event['uuid']= ''; $event['format'] = external_util::validate_format($event['format']); if ($event['repeats'] > 0) { $event['repeat'] = 1; } else { $event['repeat'] = 0; } $eventobj = new calendar_event($event); // Let's check if the user is allowed to delete an event. if (!calendar_add_event_allowed($eventobj)) { $warnings [] = array('item' => $event['name'], 'warningcode' => 'nopermissions', 'message' => 'you do not have permissions to create this event'); continue; } // Let's create the event. $var = $eventobj->create($event); $var = (array)$var->properties(); if ($event['repeat']) { $children = $DB->get_records('event', array('repeatid' => $var['id'])); foreach ($children as $child) { $return[] = (array) $child; } } else { $return[] = $var; } } // Everything done smoothly, let's commit. $transaction->allow_commit(); return array('events' => $return, 'warnings' => $warnings); } /** * Returns description of method result value. * * @return \core_external\external_description. * @since Moodle 2.5 */ public static function create_calendar_events_returns() { return new external_single_structure( array( 'events' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'event id'), 'name' => new external_value(PARAM_RAW, 'event name'), 'description' => new external_value(PARAM_RAW, 'Description', VALUE_OPTIONAL), 'format' => new external_format_value('description'), 'courseid' => new external_value(PARAM_INT, 'course id'), 'groupid' => new external_value(PARAM_INT, 'group id'), 'userid' => new external_value(PARAM_INT, 'user id'), 'repeatid' => new external_value(PARAM_INT, 'repeat id', VALUE_OPTIONAL), 'modulename' => new external_value(PARAM_TEXT, 'module name', VALUE_OPTIONAL), 'instance' => new external_value(PARAM_INT, 'instance id'), 'eventtype' => new external_value(PARAM_TEXT, 'Event type'), 'timestart' => new external_value(PARAM_INT, 'timestart'), 'timeduration' => new external_value(PARAM_INT, 'time duration'), 'visible' => new external_value(PARAM_INT, 'visible'), 'uuid' => new external_value(PARAM_TEXT, 'unique id of ical events', VALUE_OPTIONAL, '', NULL_NOT_ALLOWED), 'sequence' => new external_value(PARAM_INT, 'sequence'), 'timemodified' => new external_value(PARAM_INT, 'time modified'), 'subscriptionid' => new external_value(PARAM_INT, 'Subscription id', VALUE_OPTIONAL), ), 'event') ), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters. * * @return external_function_parameters */ public static function get_calendar_event_by_id_parameters() { return new external_function_parameters( array( 'eventid' => new external_value(PARAM_INT, 'The event id to be retrieved'), ) ); } /** * Get calendar event by id. * * @param int $eventid The calendar event id to be retrieved. * @return array Array of event details */ public static function get_calendar_event_by_id($eventid) { global $PAGE, $USER; $params = self::validate_parameters(self::get_calendar_event_by_id_parameters(), ['eventid' => $eventid]); $context = \context_user::instance($USER->id); self::validate_context($context); $warnings = array(); $eventvault = event_container::get_event_vault(); if ($event = $eventvault->get_event_by_id($params['eventid'])) { $mapper = event_container::get_event_mapper(); if (!calendar_view_event_allowed($mapper->from_event_to_legacy_event($event))) { throw new moodle_exception('nopermissiontoviewcalendar', 'error'); } } if (!$event) { // We can't return a warning in this case because the event is not optional. // We don't know the context for the event and it's not worth loading it. $syscontext = context_system::instance(); throw new \required_capability_exception($syscontext, 'moodle/course:view', 'nopermissions', 'error'); } $cache = new events_related_objects_cache([$event]); $relatedobjects = [ 'context' => $cache->get_context($event), 'course' => $cache->get_course($event), ]; $exporter = new event_exporter($event, $relatedobjects); $renderer = $PAGE->get_renderer('core_calendar'); return array('event' => $exporter->export($renderer), 'warnings' => $warnings); } /** * Returns description of method result value * * @return \core_external\external_description */ public static function get_calendar_event_by_id_returns() { $eventstructure = event_exporter::get_read_structure(); return new external_single_structure(array( 'event' => $eventstructure, 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters. * * @return external_function_parameters. */ public static function submit_create_update_form_parameters() { return new external_function_parameters( [ 'formdata' => new external_value(PARAM_RAW, 'The data from the event form'), ] ); } /** * Handles the event form submission. * * @param string $formdata The event form data in a URI encoded param string * @return array The created or modified event * @throws moodle_exception */ public static function submit_create_update_form($formdata) { global $USER, $PAGE, $CFG; require_once($CFG->libdir."/filelib.php"); // Parameter validation. $params = self::validate_parameters(self::submit_create_update_form_parameters(), ['formdata' => $formdata]); $context = \context_user::instance($USER->id); $data = []; self::validate_context($context); parse_str($params['formdata'], $data); if (WS_SERVER) { // Request via WS, ignore sesskey checks in form library. $USER->ignoresesskey = true; } $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null; $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid'; $courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null; $editoroptions = \core_calendar\local\event\forms\create::build_editor_options($context); $formoptions = ['editoroptions' => $editoroptions, 'courseid' => $courseid]; $allowedeeventtypes = calendar_get_allowed_event_types($courseid); // Event type validation. if (in_array(true, $allowedeeventtypes, true) === false) { throw new \moodle_exception('nopermissiontoupdatecalendar'); } if (empty($eventtype) || !isset($allowedeeventtypes[$eventtype]) || $allowedeeventtypes[$eventtype] == false) { return ['validationerror' => true]; } $formoptions['eventtypes'] = $allowedeeventtypes; if ($courseid) { require_once($CFG->libdir . '/grouplib.php'); $groupcoursedata = groups_get_all_groups($courseid); if (!empty($groupcoursedata)) { $formoptions['groups'] = []; foreach ($groupcoursedata as $groupid => $groupdata) { $formoptions['groups'][$groupid] = $groupdata->name; } } } if (!empty($data['id'])) { $eventid = clean_param($data['id'], PARAM_INT); $legacyevent = calendar_event::load($eventid); $legacyevent->count_repeats(); $formoptions['event'] = $legacyevent; $mform = new update_event_form(null, $formoptions, 'post', '', null, true, $data); } else { $legacyevent = null; $mform = new create_event_form(null, $formoptions, 'post', '', null, true, $data); } if ($validateddata = $mform->get_data()) { $formmapper = new create_update_form_mapper(); $properties = $formmapper->from_data_to_event_properties($validateddata); if (is_null($legacyevent)) { $legacyevent = new \calendar_event($properties); // Need to do this in order to initialise the description // property which then triggers the update function below // to set the appropriate default properties on the event. $properties = $legacyevent->properties(true); } if (!calendar_edit_event_allowed($legacyevent, true)) { throw new \moodle_exception('nopermissiontoupdatecalendar'); } $legacyevent->update($properties); $eventcontext = $legacyevent->context; file_remove_editor_orphaned_files($validateddata->description); // Take any files added to the description draft file area and // convert them into the proper event description file area. Also // parse the description text and replace the URLs to the draft files // with the @@PLUGIN_FILE@@ placeholder to be persisted in the DB. $description = file_save_draft_area_files( $validateddata->description['itemid'], $eventcontext->id, 'calendar', 'event_description', $legacyevent->id, create_event_form::build_editor_options($eventcontext), $validateddata->description['text'] ); // If draft files were found then we need to save the new // description value. if ($description != $validateddata->description['text']) { $properties->id = $legacyevent->id; $properties->description = $description; $legacyevent->update($properties); } $eventmapper = event_container::get_event_mapper(); $event = $eventmapper->from_legacy_event_to_event($legacyevent); $cache = new events_related_objects_cache([$event]); $relatedobjects = [ 'context' => $cache->get_context($event), 'course' => $cache->get_course($event), ]; $exporter = new event_exporter($event, $relatedobjects); $renderer = $PAGE->get_renderer('core_calendar'); return [ 'event' => $exporter->export($renderer) ]; } else { return [ 'validationerror' => true ]; } } /** * Returns description of method result value. * * @return \core_external\external_description. */ public static function submit_create_update_form_returns() { $eventstructure = event_exporter::get_read_structure(); $eventstructure->required = VALUE_OPTIONAL; return new external_single_structure( array( 'event' => $eventstructure, 'validationerror' => new external_value(PARAM_BOOL, 'Invalid form data', VALUE_DEFAULT, false), ) ); } /** * Get data for the monthly calendar view. * * @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 * @param bool $mini Whether to return the mini month view or not * @param int $day The day we want to keep as the current day * @param string|null $view The view mode for the calendar. * @return array */ public static function get_calendar_monthly_view($year, $month, $courseid, $categoryid, $includenavigation, $mini, $day, ?string $view = null) { global $USER, $PAGE; // Parameter validation. $params = self::validate_parameters(self::get_calendar_monthly_view_parameters(), [ 'year' => $year, 'month' => $month, 'courseid' => $courseid, 'categoryid' => $categoryid, 'includenavigation' => $includenavigation, 'mini' => $mini, 'day' => $day, 'view' => $view, ]); $context = \context_user::instance($USER->id); self::validate_context($context); $PAGE->set_url('/calendar/'); $type = \core_calendar\type_factory::get_calendar_instance(); $time = $type->convert_to_timestamp($params['year'], $params['month'], $params['day']); $calendar = \calendar_information::create($time, $params['courseid'], $params['categoryid']); self::validate_context($calendar->context); $view = $params['view'] ?? ($params['mini'] ? 'mini' : 'month'); list($data, $template) = calendar_get_view($calendar, $view, $params['includenavigation']); return $data; } /** * Returns description of method parameters. * * @return external_function_parameters */ public static function get_calendar_monthly_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), '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', VALUE_DEFAULT, true, NULL_ALLOWED ), 'mini' => new external_value( PARAM_BOOL, 'Whether to return the mini month view or not', VALUE_DEFAULT, false, NULL_ALLOWED ), 'day' => new external_value(PARAM_INT, 'Day to be viewed', VALUE_DEFAULT, 1), 'view' => new external_value(PARAM_ALPHA, 'The view mode of the calendar', VALUE_DEFAULT, 'month', NULL_ALLOWED), ] ); } /** * Returns description of method result value. * * @return \core_external\external_description */ public static function get_calendar_monthly_view_returns() { 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 $DB, $USER, $PAGE; // Parameter validation. $params = self::validate_parameters(self::get_calendar_day_view_parameters(), [ 'year' => $year, 'month' => $month, 'day' => $day, 'courseid' => $courseid, 'categoryid' => $categoryid, ]); $context = \context_user::instance($USER->id); self::validate_context($context); $type = \core_calendar\type_factory::get_calendar_instance(); $time = $type->convert_to_timestamp($params['year'], $params['month'], $params['day']); $calendar = \calendar_information::create($time, $params['courseid'], $params['categoryid']); self::validate_context($calendar->context); list($data, $template) = calendar_get_view($calendar, 'day'); 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 \core_external\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. * * @return external_function_parameters */ public static function update_event_start_day_parameters() { return new external_function_parameters( [ 'eventid' => new external_value(PARAM_INT, 'Id of event to be updated', VALUE_REQUIRED), 'daytimestamp' => new external_value(PARAM_INT, 'Timestamp for the new start day', VALUE_REQUIRED), ] ); } /** * Change the start day for the given calendar event to the day that * corresponds with the provided timestamp. * * The timestamp only needs to be anytime within the desired day as only * the date data is extracted from it. * * The event's original time of day is maintained, only the date is shifted. * * @param int $eventid Id of event to be updated * @param int $daytimestamp Timestamp for the new start day * @return array */ public static function update_event_start_day($eventid, $daytimestamp) { global $USER, $PAGE; // Parameter validation. $params = self::validate_parameters(self::update_event_start_day_parameters(), [ 'eventid' => $eventid, 'daytimestamp' => $daytimestamp, ]); $vault = event_container::get_event_vault(); $mapper = event_container::get_event_mapper(); $event = $vault->get_event_by_id($eventid); if (!$event) { throw new \moodle_exception('Unable to find event with id ' . $eventid); } $legacyevent = $mapper->from_event_to_legacy_event($event); if (!calendar_edit_event_allowed($legacyevent, true)) { throw new \moodle_exception('nopermissiontoupdatecalendar'); } self::validate_context($legacyevent->context); $newdate = usergetdate($daytimestamp); $startdatestring = implode('-', [$newdate['year'], $newdate['mon'], $newdate['mday']]); $startdate = new DateTimeImmutable($startdatestring); $event = local_api::update_event_start_day($event, $startdate); $cache = new events_related_objects_cache([$event]); $relatedobjects = [ 'context' => $cache->get_context($event), 'course' => $cache->get_course($event), ]; $exporter = new event_exporter($event, $relatedobjects); $renderer = $PAGE->get_renderer('core_calendar'); return array('event' => $exporter->export($renderer)); } /** * Returns description of method result value. * * @return \core_external\external_description */ public static function update_event_start_day_returns() { return new external_single_structure( array( 'event' => event_exporter::get_read_structure() ) ); } /** * Get data for the monthly calendar view. * * @param int $courseid The course to be included * @param int $categoryid The category to be included * @return array */ public static function get_calendar_upcoming_view($courseid, $categoryid) { global $DB, $USER, $PAGE; // Parameter validation. $params = self::validate_parameters(self::get_calendar_upcoming_view_parameters(), [ 'courseid' => $courseid, 'categoryid' => $categoryid, ]); $context = \context_user::instance($USER->id); self::validate_context($context); $PAGE->set_url('/calendar/'); $calendar = \calendar_information::create(time(), $params['courseid'], $params['categoryid']); self::validate_context($calendar->context); 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), 'categoryid' => new external_value(PARAM_INT, 'Category being viewed', VALUE_DEFAULT, null, NULL_ALLOWED), ] ); } /** * Returns description of method result value. * * @return \core_external\external_description */ public static function get_calendar_upcoming_view_returns() { return \core_calendar\external\calendar_upcoming_exporter::get_read_structure(); } /** * Returns description of method parameters. * * @return external_function_parameters. * @since Moodle 3.7 */ public static function get_calendar_access_information_parameters() { return new external_function_parameters( [ 'courseid' => new external_value(PARAM_INT, 'Course to check, empty for site calendar events.', VALUE_DEFAULT, 0), ] ); } /** * Convenience function to retrieve some permissions information for the given course calendar. * * @param int $courseid Course to check, empty for site. * @return array The access information * @throws moodle_exception * @since Moodle 3.7 */ public static function get_calendar_access_information($courseid = 0) { $params = self::validate_parameters(self::get_calendar_access_information_parameters(), ['courseid' => $courseid]); if (empty($params['courseid']) || $params['courseid'] == SITEID) { $context = \context_system::instance(); } else { $context = \context_course::instance($params['courseid']); } self::validate_context($context); return [ 'canmanageentries' => has_capability('moodle/calendar:manageentries', $context), 'canmanageownentries' => has_capability('moodle/calendar:manageownentries', $context), 'canmanagegroupentries' => has_capability('moodle/calendar:managegroupentries', $context), 'warnings' => [], ]; } /** * Returns description of method result value. * * @return \core_external\external_description. * @since Moodle 3.7 */ public static function get_calendar_access_information_returns() { return new external_single_structure( [ 'canmanageentries' => new external_value(PARAM_BOOL, 'Whether the user can manage entries.'), 'canmanageownentries' => new external_value(PARAM_BOOL, 'Whether the user can manage its own entries.'), 'canmanagegroupentries' => new external_value(PARAM_BOOL, 'Whether the user can manage group entries.'), 'warnings' => new external_warnings(), ] ); } /** * Returns description of method parameters. * * @return external_function_parameters. * @since Moodle 3.7 */ public static function get_allowed_event_types_parameters() { return new external_function_parameters( [ 'courseid' => new external_value(PARAM_INT, 'Course to check, empty for site.', VALUE_DEFAULT, 0), ] ); } /** * Get the type of events a user can create in the given course. * * @param int $courseid Course to check, empty for site. * @return array The types allowed * @throws moodle_exception * @since Moodle 3.7 */ public static function get_allowed_event_types($courseid = 0) { $params = self::validate_parameters(self::get_allowed_event_types_parameters(), ['courseid' => $courseid]); if (empty($params['courseid']) || $params['courseid'] == SITEID) { $context = \context_system::instance(); } else { $context = \context_course::instance($params['courseid']); } self::validate_context($context); $allowedeventtypes = array_filter(calendar_get_allowed_event_types($params['courseid'])); return [ 'allowedeventtypes' => array_keys($allowedeventtypes), 'warnings' => [], ]; } /** * Returns description of method result value. * * @return \core_external\external_description. * @since Moodle 3.7 */ public static function get_allowed_event_types_returns() { return new external_single_structure( [ 'allowedeventtypes' => new external_multiple_structure( new external_value(PARAM_NOTAGS, 'Allowed event types to be created in the given course.') ), 'warnings' => new external_warnings(), ] ); } /** * Convert the specified dates into unix timestamps. * * @param array $datetimes Array of arrays containing date time details, each in the format: * ['year' => a, 'month' => b, 'day' => c, * 'hour' => d (optional), 'minute' => e (optional), 'key' => 'x' (optional)] * @return array Provided array of dates converted to unix timestamps * @throws moodle_exception If one or more of the dates provided does not convert to a valid timestamp. */ public static function get_timestamps($datetimes) { $params = self::validate_parameters(self::get_timestamps_parameters(), ['data' => $datetimes]); $type = \core_calendar\type_factory::get_calendar_instance(); $timestamps = ['timestamps' => []]; foreach ($params['data'] as $key => $datetime) { $hour = $datetime['hour'] ?? 0; $minute = $datetime['minute'] ?? 0; try { $timestamp = $type->convert_to_timestamp( $datetime['year'], $datetime['month'], $datetime['day'], $hour, $minute); $timestamps['timestamps'][] = [ 'key' => $datetime['key'] ?? $key, 'timestamp' => $timestamp, ]; } catch (Exception $e) { throw new moodle_exception('One or more of the dates provided were invalid'); } } return $timestamps; } /** * Describes the parameters for get_timestamps. * * @return external_function_parameters */ public static function get_timestamps_parameters() { return new external_function_parameters ([ 'data' => new external_multiple_structure( new external_single_structure( [ 'key' => new external_value(PARAM_ALPHANUMEXT, 'key', VALUE_OPTIONAL), 'year' => new external_value(PARAM_INT, 'year'), 'month' => new external_value(PARAM_INT, 'month'), 'day' => new external_value(PARAM_INT, 'day'), 'hour' => new external_value(PARAM_INT, 'hour', VALUE_OPTIONAL), 'minute' => new external_value(PARAM_INT, 'minute', VALUE_OPTIONAL), ] ) ) ]); } /** * Describes the timestamps return format. * * @return external_single_structure */ public static function get_timestamps_returns() { return new external_single_structure( [ 'timestamps' => new external_multiple_structure( new external_single_structure( [ 'key' => new external_value(PARAM_ALPHANUMEXT, 'Timestamp key'), 'timestamp' => new external_value(PARAM_INT, 'Unix timestamp'), ] ) ) ] ); } } PK*\o1o1 renderer.phpnu[. /** * This file contains the renderers for the calendar within Moodle * * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package calendar */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } /** * The primary renderer for the calendar. */ class core_calendar_renderer extends plugin_renderer_base { /** * Starts the standard layout for the page * * @return string */ public function start_layout() { return html_writer::start_tag('div', ['data-region' => 'calendar', 'class' => 'maincalendar']); } /** * Creates the remainder of the layout * * @return string */ public function complete_layout() { return html_writer::end_tag('div'); } /** * Adds a pretent calendar block * * @param block_contents $bc * @param mixed $pos BLOCK_POS_RIGHT | BLOCK_POS_LEFT */ public function add_pretend_calendar_block(block_contents $bc, $pos=BLOCK_POS_RIGHT) { $this->page->blocks->add_fake_block($bc, $pos); } /** * Creates a button to add a new event. * * @param int $courseid * @param int $unused1 * @param int $unused2 * @param int $unused3 * @param int $unused4 * @return string */ public function add_event_button($courseid, $unused1 = null, $unused2 = null, $unused3 = null, $unused4 = null) { $data = [ 'contextid' => (\context_course::instance($courseid))->id, ]; return $this->render_from_template('core_calendar/add_event_button', $data); } /** * @deprecated 3.9 */ #[\core\attribute\deprecated( replacement: 'event no longer used', since: '3.9', mdl: 'MDL-58866', final: true, )] public function event() { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * Displays a course filter selector * * @param moodle_url $returnurl The URL that the user should be taken too upon selecting a course. * @param string $label The label to use for the course select. * @param int $courseid The id of the course to be selected. * @param int|null $calendarinstanceid The instance ID of the calendar we're generating this course filter for. * @return string */ public function course_filter_selector(moodle_url $returnurl, $label = null, $courseid = null, ?int $calendarinstanceid = null) { global $CFG, $DB; if (!isloggedin() or isguestuser()) { return ''; } $contextrecords = []; $courses = calendar_get_default_courses($courseid, 'id, shortname, fullname'); if (!empty($courses) && count($courses) > CONTEXT_CACHE_MAX_SIZE) { // We need to pull the context records from the DB to preload them // below. The calendar_get_default_courses code will actually preload // the contexts itself however the context cache is capped to a certain // amount before it starts recycling. Unfortunately that starts to happen // quite a bit if a user has access to a large number of courses (e.g. admin). // So in order to avoid hitting the DB for each context as we loop below we // can load all of the context records and add them to the cache just in time. $courseids = array_map(function($c) { return $c->id; }, $courses); list($insql, $params) = $DB->get_in_or_equal($courseids); $contextsql = "SELECT ctx.instanceid, " . context_helper::get_preload_record_columns_sql('ctx') . " FROM {context} ctx WHERE ctx.contextlevel = ? AND ctx.instanceid $insql"; array_unshift($params, CONTEXT_COURSE); $contextrecords = $DB->get_records_sql($contextsql, $params); } unset($courses[SITEID]); $courseoptions = array(); $courseoptions[SITEID] = get_string('fulllistofcourses'); foreach ($courses as $course) { if (isset($contextrecords[$course->id])) { context_helper::preload_from_record($contextrecords[$course->id]); } // Limit the displayed course name to prevent the dropdown from getting too wide. $coursename = format_string(get_course_display_name_for_list($course), true, [ 'context' => \core\context\course::instance($course->id), ]); $courseoptions[$course->id] = shorten_text($coursename, 50, true); } if ($courseid) { $selected = $courseid; } else if ($this->page->course->id !== SITEID) { $selected = $this->page->course->id; } else { $selected = ''; } $courseurl = new moodle_url($returnurl); $courseurl->remove_params('course'); $labelattributes = []; if (empty($label)) { $label = get_string('listofcourses'); $labelattributes['class'] = 'visually-hidden'; } $filterid = 'calendar-course-filter'; if ($calendarinstanceid) { $filterid .= "-$calendarinstanceid"; } $select = html_writer::label($label, $filterid, false, $labelattributes); $select .= html_writer::select($courseoptions, 'course', $selected, false, ['class' => 'cal_courses_flt ms-1 me-auto me-2 mb-2', 'id' => $filterid]); return $select; } /** * Render the subscriptions header * * @return string */ public function render_subscriptions_header(): string { $importcalendarbutton = new single_button(new moodle_url('/calendar/import.php', calendar_get_export_import_link_params()), get_string('importcalendar', 'calendar'), 'get', single_button::BUTTON_PRIMARY); $importcalendarbutton->class .= ' float-sm-end float-end'; $exportcalendarbutton = new single_button(new moodle_url('/calendar/export.php', calendar_get_export_import_link_params()), get_string('exportcalendar', 'calendar'), 'get', single_button::BUTTON_PRIMARY); $exportcalendarbutton->class .= ' float-sm-end float-end'; $output = $this->output->heading(get_string('managesubscriptions', 'calendar')); $output .= html_writer::start_div('header d-flex flex-wrap mt-5'); $headerattr = [ 'class' => 'me-auto', 'aria-describedby' => 'subscription_details_table', ]; $output .= html_writer::tag('h3', get_string('yoursubscriptions', 'calendar'), $headerattr); $output .= $this->output->render($importcalendarbutton); $output .= $this->output->render($exportcalendarbutton); $output .= html_writer::end_div(); return $output; } /** * Render the subscriptions blank state appearance * * @return string */ public function render_no_calendar_subscriptions(): string { $output = html_writer::start_div('mt-5'); $importlink = (new moodle_url('/calendar/import.php', calendar_get_export_import_link_params()))->out(); $output .= get_string('nocalendarsubscriptionsimportexternal', 'core_calendar', $importlink); $output .= html_writer::end_div(); return $output; } /** * Renders a table containing information about calendar subscriptions. * * @param int $unused * @param array $subscriptions * @param string $unused2 * @return string */ public function subscription_details($unused, $subscriptions, $unused2 = '') { $table = new html_table(); $table->head = array( get_string('colcalendar', 'calendar'), get_string('collastupdated', 'calendar'), get_string('eventkind', 'calendar'), get_string('colpoll', 'calendar'), get_string('colactions', 'calendar') ); $table->data = array(); $table->id = 'subscription_details_table'; if (empty($subscriptions)) { $importlink = (new moodle_url('/calendar/import.php', calendar_get_export_import_link_params()))->out(); $cell = new html_table_cell(get_string('nocalendarsubscriptionsimportexternal', 'core_calendar', $importlink)); $cell->colspan = 5; $table->data[] = new html_table_row(array($cell)); } $strnever = new lang_string('never', 'calendar'); foreach ($subscriptions as $sub) { $label = $sub->name; if (!empty($sub->url)) { $label = html_writer::link($sub->url, $label); } if (empty($sub->lastupdated)) { $lastupdated = $strnever->out(); } else { $lastupdated = userdate($sub->lastupdated, get_string('strftimedatetimeshort', 'langconfig')); } $type = $sub->eventtype . 'events'; $calendarname = new html_table_cell($label); $calendarname->header = true; $tablerow = new html_table_row(array( $calendarname, new html_table_cell($lastupdated), new html_table_cell(get_string($type, 'calendar')), new html_table_cell($this->render_subscription_update_interval($sub)), new html_table_cell($this->subscription_action_links()) )); $tablerow->attributes += [ 'data-subid' => $sub->id, 'data-subname' => $sub->name ]; $table->data[] = $tablerow; } $out = $this->output->box_start('generalbox calendarsubs'); $out .= html_writer::table($table); $out .= $this->output->box_end(); $this->page->requires->js_call_amd('core_calendar/manage_subscriptions', 'init'); return $out; } /** * Render subscription update interval form. * * @param stdClass $subscription * @return string */ protected function render_subscription_update_interval(stdClass $subscription): string { if (empty($subscription->url)) { return ''; } $tmpl = new \core_calendar\output\refreshintervalcollection($subscription); return $this->output->render_from_template('core/inplace_editable', $tmpl->export_for_template($this->output)); } /** * Creates a form to perform actions on a given subscription. * * @return string */ protected function subscription_action_links(): string { $html = html_writer::start_tag('div', array('class' => 'btn-group float-start')); $html .= html_writer::span(html_writer::link('#', get_string('delete'), ['data-action' => 'delete-subscription']), ''); $html .= html_writer::end_tag('div'); return $html; } /** * Render the event filter region. * * @return string */ public function event_filter() { $data = [ 'eventtypes' => calendar_get_filter_types(), ]; return $this->render_from_template('core_calendar/event_filter', $data); } /** * Render the calendar import result. * * @param array $result Import result * @return string|null */ public function render_import_result(array $result): ?string { $data = [ 'eventsimported' => $result['eventsimported'], 'eventsskipped' => $result['eventsskipped'], 'eventsupdated' => $result['eventsupdated'], 'eventsdeleted' => $result['eventsdeleted'], 'haserror' => $result['haserror'], 'errors' => $result['errors'] ]; return $this->render_from_template('core_calendar/subscription_update_result', $data); } } PK*\G9view.phpnu[dirroot.'/course/lib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); $categoryid = optional_param('category', null, PARAM_INT); $courseid = optional_param('course', SITEID, PARAM_INT); $view = optional_param('view', 'upcoming', PARAM_ALPHA); $day = optional_param('cal_d', 0, PARAM_INT); $mon = optional_param('cal_m', 0, PARAM_INT); $year = optional_param('cal_y', 0, PARAM_INT); $time = optional_param('time', 0, PARAM_INT); $lookahead = optional_param('lookahead', null, PARAM_INT); $url = new moodle_url('/calendar/view.php'); // 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($day) && !empty($mon) && !empty($year)) { if (checkdate($mon, $day, $year)) { $time = make_timestamp($year, $mon, $day); } } if (empty($time)) { $time = time(); } $iscoursecalendar = $courseid != SITEID; if ($iscoursecalendar) { $url->param('course', $courseid); } if ($categoryid) { $url->param('categoryid', $categoryid); } if ($view !== 'upcoming') { $time = usergetmidnight($time); $url->param('view', $view); } $url->param('time', $time); $PAGE->set_url($url); $course = get_course($courseid); if ($iscoursecalendar && !empty($courseid)) { navigation_node::override_active_url(new moodle_url('/course/view.php', array('id' => $course->id))); $PAGE->set_secondary_navigation(false); } else if (!empty($categoryid)) { core_course_category::get($categoryid); // Check that category exists and can be accessed. $PAGE->set_category_by_id($categoryid); navigation_node::override_active_url(new moodle_url('/course/index.php', array('categoryid' => $categoryid))); $PAGE->navbar->add( get_string('calendar', 'calendar'), new moodle_url('/calendar/view.php', ['view' => 'month', 'category' => $categoryid]) ); $PAGE->set_secondary_navigation(false); } else { $PAGE->set_context(context_system::instance()); } // Auto log in guests on frontpage. $autologinguest = !$iscoursecalendar; require_login($course, $autologinguest); $calendar = calendar_information::create($time, $courseid, $categoryid); $pagetitle = ''; $strcalendar = get_string('calendar', 'calendar'); switch($view) { case 'day': $PAGE->navbar->add(userdate($time, get_string('strftimedate'))); $pagetitle = get_string('dayviewtitle', 'calendar', userdate($time, get_string('strftimedaydate'))); break; case 'month': $PAGE->navbar->add(userdate($time, get_string('strftimemonthyear'))); $pagetitle = get_string('detailedmonthviewtitle', 'calendar', userdate($time, get_string('strftimemonthyear'))); break; case 'upcoming': $pagetitle = get_string('upcomingevents', 'calendar'); break; } $PAGE->set_show_course_index(false); $PAGE->set_pagelayout('standard'); // Print title and header. $PAGE->set_title("$course->shortname: $strcalendar: $pagetitle"); $headingstr = get_string('calendar', 'core_calendar'); // If the user is on the course page, // then make the course name linkable to ease the user's navigation to the course page. if ($iscoursecalendar) { $linkcourse = html_writer::link( course_get_url($course), format_string($course->shortname, options: ['context' => $PAGE->context]), ); $headingstr = "{$headingstr}: {$linkcourse}"; } $PAGE->set_heading($headingstr, false); $renderer = $PAGE->get_renderer('core_calendar'); $calendar->add_sidecalendar_blocks($renderer, true, $view); echo $OUTPUT->header(); echo $renderer->start_layout(); echo html_writer::start_tag('div', ['class' => 'heightcontainer', 'data-calendar-type' => 'main-block']); list($data, $template) = calendar_get_view($calendar, $view, true, false, $lookahead); echo $renderer->render_from_template($template, $data); echo html_writer::end_tag('div'); list($data, $template) = calendar_get_footer_options($calendar); echo $renderer->render_from_template($template, $data); echo $renderer->complete_layout(); echo $OUTPUT->footer(); PK*\v2 delete.phpnu[. /** * This file is part of the Calendar section Moodle * It is responsible for deleting a calendar entry + optionally its repeats * * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package calendar */ require_once('../config.php'); require_once($CFG->dirroot.'/calendar/event_form.php'); require_once($CFG->dirroot.'/calendar/lib.php'); require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->dirroot.'/calendar/renderer.php'); $eventid = required_param('id', PARAM_INT); $confirm = optional_param('confirm', false, PARAM_BOOL); $repeats = optional_param('repeats', false, PARAM_BOOL); $courseid = optional_param('course', 0, PARAM_INT); $PAGE->set_url('/calendar/delete.php', array('id'=>$eventid)); if(!$site = get_site()) { redirect(new moodle_url('/admin/index.php')); } $event = calendar_event::load($eventid); /** * We are going to be picky here, and require that any event types other than * group and site be associated with a course. This means any code that is using * custom event types (and there are a few) will need to associate thier event with * a course */ if ($event->eventtype !== 'user' && $event->eventtype !== 'site') { $courseid = $event->courseid; } $course = $DB->get_record('course', array('id'=>$courseid)); require_login($course); if (!$course) { $PAGE->set_context(context_system::instance()); //TODO: wrong } $title = get_string('deleteevent', 'calendar'); // Check the user has the required capabilities to delete an event if (!calendar_delete_event_allowed($event)) { throw new \moodle_exception('nopermissions', 'error', $PAGE->url, $title); } // Count the repeats, do we need to consider the possibility of deleting repeats $event->timedurationuntil = $event->timestart + $event->timeduration; $event->count_repeats(); // Is used several times, and sometimes with modification if required $viewcalendarurl = new moodle_url(CALENDAR_URL.'view.php', array('view'=>'upcoming')); $viewcalendarurl->param('time', $event->timestart, '%Y'); // If confirm is set (PARAM_BOOL) then we have confirmation of initention to delete if ($confirm) { // Confirm the session key to stop CSRF if (!confirm_sesskey()) { throw new \moodle_exception('confirmsesskeybad'); } // Delete the event and possibly repeats $event->delete($repeats); // If the event has an associated course then we need to include it in the redirect link if (!empty($event->courseid) && $event->courseid > 0) { $viewcalendarurl->param('course', $event->courseid); } // And redirect redirect($viewcalendarurl); } // Prepare the page to show the confirmation form $strcalendar = get_string('calendar', 'calendar'); $PAGE->navbar->add($strcalendar, $viewcalendarurl); $PAGE->navbar->add($title); $PAGE->set_title($strcalendar.': '.$title); $PAGE->set_heading($COURSE->fullname); if ($course) { $PAGE->set_secondary_navigation(false); } echo $OUTPUT->header(); echo $OUTPUT->box_start('eventlist'); // Delete this event button is always shown $url = new moodle_url(CALENDAR_URL.'delete.php', array('id'=>$event->id, 'confirm'=>true)); $buttons = $OUTPUT->single_button($url, get_string('delete')); // And add the cancel button. $buttons .= $OUTPUT->single_button($viewcalendarurl, get_string('cancel')); // And show the buttons and notes. echo $OUTPUT->box_start('generalbox', 'notice'); echo html_writer::tag('p', get_string('confirmeventdelete', 'calendar', format_string($event->name))); // If there are repeated events then add a Delete Repeated button. if (!empty($event->eventrepeats) && $event->eventrepeats > 0) { $url = new moodle_url(CALENDAR_URL.'delete.php', array('id'=>$event->repeatid, 'confirm'=>true, 'repeats'=>true)); $buttons .= $OUTPUT->single_button($url, get_string('deleteall')); echo html_writer::tag('p', get_string('youcandeleteallrepeats', 'calendar', $event->eventrepeats)); } echo $OUTPUT->box($buttons, 'buttons'); echo $OUTPUT->box_end(); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); PK*\hBlib.phpnu[. /** * Calendar extension * * @package core_calendar * @copyright 2004 Greek School Network (http://www.sch.gr), Jon Papaioannou, * Avgoustos Tsinakos * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_external\external_api; if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } require_once(__DIR__ . '/deprecatedlib.php'); /** * These are read by the administration component to provide default values */ /** * CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD - default value of upcoming event preference */ define('CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD', 21); /** * CALENDAR_DEFAULT_UPCOMING_MAXEVENTS - default value to display the maximum number of upcoming event */ define('CALENDAR_DEFAULT_UPCOMING_MAXEVENTS', 10); /** * CALENDAR_DEFAULT_STARTING_WEEKDAY - default value to display the starting weekday */ define('CALENDAR_DEFAULT_STARTING_WEEKDAY', 1); // This is a packed bitfield: day X is "weekend" if $field & (1 << X) is true // Default value = 65 = 64 + 1 = 2^6 + 2^0 = Saturday & Sunday /** * CALENDAR_DEFAULT_WEEKEND - default value for weekend (Saturday & Sunday) */ define('CALENDAR_DEFAULT_WEEKEND', 65); /** * CALENDAR_URL - path to calendar's folder */ define('CALENDAR_URL', $CFG->wwwroot.'/calendar/'); /** * CALENDAR_TF_24 - Calendar time in 24 hours format */ define('CALENDAR_TF_24', '%H:%M'); /** * CALENDAR_TF_12 - Calendar time in 12 hours format */ define('CALENDAR_TF_12', '%I:%M %p'); /** * CALENDAR_EVENT_SITE - Site calendar event types */ define('CALENDAR_EVENT_SITE', 1); /** * CALENDAR_EVENT_COURSE - Course calendar event types */ define('CALENDAR_EVENT_COURSE', 2); /** * CALENDAR_EVENT_GROUP - group calendar event types */ define('CALENDAR_EVENT_GROUP', 4); /** * CALENDAR_EVENT_USER - user calendar event types */ 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 */ define('CALENDAR_IMPORT_FROM_FILE', 0); /** * CALENDAR_IMPORT_FROM_URL - import the calendar from a URL */ define('CALENDAR_IMPORT_FROM_URL', 1); /** * CALENDAR_IMPORT_EVENT_UPDATED_SKIPPED - imported event was skipped */ define('CALENDAR_IMPORT_EVENT_SKIPPED', -1); /** * CALENDAR_IMPORT_EVENT_UPDATED - imported event was updated */ define('CALENDAR_IMPORT_EVENT_UPDATED', 1); /** * CALENDAR_IMPORT_EVENT_INSERTED - imported event was added by insert */ define('CALENDAR_IMPORT_EVENT_INSERTED', 2); /** * CALENDAR_SUBSCRIPTION_UPDATE - Used to represent update action for subscriptions in various forms. */ define('CALENDAR_SUBSCRIPTION_UPDATE', 1); /** * CALENDAR_SUBSCRIPTION_REMOVE - Used to represent remove action for subscriptions in various forms. */ define('CALENDAR_SUBSCRIPTION_REMOVE', 2); /** * CALENDAR_EVENT_USER_OVERRIDE_PRIORITY - Constant for the user override priority. */ define('CALENDAR_EVENT_USER_OVERRIDE_PRIORITY', 0); /** * CALENDAR_EVENT_TYPE_STANDARD - Standard events. */ define('CALENDAR_EVENT_TYPE_STANDARD', 0); /** * CALENDAR_EVENT_TYPE_ACTION - Action events. */ define('CALENDAR_EVENT_TYPE_ACTION', 1); /** * Manage calendar events. * * This class provides the required functionality in order to manage calendar events. * It was introduced as part of Moodle 2.0 and was created in order to provide a * better framework for dealing with calendar events in particular regard to file * handling through the new file API. * * @package core_calendar * @category calendar * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * * @property int $id The id within the event table * @property string $name The name of the event * @property string $description The description of the event * @property int $format The format of the description FORMAT_? * @property int $courseid The course the event is associated with (0 if none) * @property int $groupid The group the event is associated with (0 if none) * @property int $userid The user the event is associated with (0 if none) * @property int $repeatid If this is a repeated event this will be set to the * id of the original * @property string $component If created by a plugin/component (other than module), the full frankenstyle name of a component * @property string $modulename If added by a module this will be the module name * @property int $instance If added by a module this will be the module instance * @property string $eventtype The event type * @property int $timestart The start time as a timestamp * @property int $timeduration The duration of the event in seconds * @property int $timeusermidnight User midnight for the event * @property int $visible 1 if the event is visible * @property int $uuid ? * @property int $sequence ? * @property int $timemodified The time last modified as a timestamp */ class calendar_event { /** @var stdClass An object containing the event properties can be accessed via the magic __get/set methods */ protected $properties = null; /** @var string The converted event discription with file paths resolved. * This gets populated when someone requests description for the first time */ protected $_description = null; /** @var array The options to use with this description editor */ protected $editoroptions = array( 'subdirs' => false, 'forcehttps' => false, 'maxfiles' => -1, 'maxbytes' => null, 'trusttext' => false); /** @var object The context to use with the description editor */ protected $editorcontext = null; /** * Instantiates a new event and optionally populates its properties with the data provided. * * @param \stdClass $data Optional. An object containing the properties to for * an event */ public function __construct($data = null) { global $CFG, $USER; // First convert to object if it is not already (should either be object or assoc array). if (!is_object($data)) { $data = (object) $data; } $this->editoroptions['maxbytes'] = $CFG->maxbytes; $data->eventrepeats = 0; if (empty($data->id)) { $data->id = null; } if (!empty($data->subscriptionid)) { $data->subscription = calendar_get_subscription($data->subscriptionid); } // Default to a user event. if (empty($data->eventtype)) { $data->eventtype = 'user'; } // Default to the current user. if (empty($data->userid)) { $data->userid = $USER->id; } if (!empty($data->timeduration) && is_array($data->timeduration)) { $data->timeduration = make_timestamp( $data->timeduration['year'], $data->timeduration['month'], $data->timeduration['day'], $data->timeduration['hour'], $data->timeduration['minute']) - $data->timestart; } if (!empty($data->description) && is_array($data->description)) { $data->format = $data->description['format']; $data->description = $data->description['text']; } else if (empty($data->description)) { $data->description = ''; $data->format = editors_get_preferred_format(); } // Ensure form is defaulted correctly. if (empty($data->format)) { $data->format = editors_get_preferred_format(); } if (empty($data->component)) { $data->component = null; } $this->properties = $data; } /** * Magic set method. * * Attempts to call a set_$key method if one exists otherwise falls back * to simply set the property. * * @param string $key property name * @param mixed $value value of the property */ public function __set($key, $value) { if (method_exists($this, 'set_'.$key)) { $this->{'set_'.$key}($value); } $this->properties->{$key} = $value; } /** * Magic get method. * * Attempts to call a get_$key method to return the property and ralls over * to return the raw property. * * @param string $key property name * @return mixed property value * @throws \coding_exception */ public function __get($key) { if (method_exists($this, 'get_'.$key)) { return $this->{'get_'.$key}(); } if (!property_exists($this->properties, $key)) { throw new \coding_exception('Undefined property requested'); } return $this->properties->{$key}; } /** * Magic isset method. * * PHP needs an isset magic method if you use the get magic method and * still want empty calls to work. * * @param string $key $key property name * @return bool|mixed property value, false if property is not exist */ public function __isset($key) { return !empty($this->properties->{$key}); } /** * Calculate the context value needed for an event. * * Event's type can be determine by the available value store in $data * It is important to check for the existence of course/courseid to determine * the course event. * Default value is set to CONTEXT_USER * * @return \stdClass The context object. */ protected function calculate_context() { global $USER, $DB; $context = null; 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); } else if (isset($this->properties->groupid) && $this->properties->groupid > 0) { $group = $DB->get_record('groups', array('id' => $this->properties->groupid)); $context = \context_course::instance($group->courseid); } else if (isset($this->properties->userid) && $this->properties->userid > 0 && $this->properties->userid == $USER->id) { $context = \context_user::instance($this->properties->userid); } else if (isset($this->properties->userid) && $this->properties->userid > 0 && $this->properties->userid != $USER->id && !empty($this->properties->modulename) && isset($this->properties->instance) && $this->properties->instance > 0) { $cm = get_coursemodule_from_instance($this->properties->modulename, $this->properties->instance, 0, false, MUST_EXIST); $context = \context_course::instance($cm->course); } else { $context = \context_user::instance($this->properties->userid); } return $context; } /** * Returns the context for this event. The context is calculated * the first time is is requested and then stored in a member * variable to be returned each subsequent time. * * This is a magical getter function that will be called when * ever the context property is accessed, e.g. $event->context. * * @return context */ protected function get_context() { if (!isset($this->properties->context)) { $this->properties->context = $this->calculate_context(); } return $this->properties->context; } /** * Returns an array of editoroptions for this event. * * @return array event editor options */ protected function get_editoroptions() { return $this->editoroptions; } /** * Returns an event description: Called by __get * Please use $blah = $event->description; * * @return string event description */ protected function get_description() { global $CFG; require_once($CFG->libdir . '/filelib.php'); if ($this->_description === null) { // Check if we have already resolved the context for this event. if ($this->editorcontext === null) { // Switch on the event type to decide upon the appropriate context to use for this event. $this->editorcontext = $this->get_context(); if (!calendar_is_valid_eventtype($this->properties->eventtype)) { return clean_text($this->properties->description, $this->properties->format); } } // Work out the item id for the editor, if this is a repeated event // then the files will be associated with the original. if (!empty($this->properties->repeatid) && $this->properties->repeatid > 0) { $itemid = $this->properties->repeatid; } else { $itemid = $this->properties->id; } // Convert file paths in the description so that things display correctly. $this->_description = file_rewrite_pluginfile_urls($this->properties->description, 'pluginfile.php', $this->editorcontext->id, 'calendar', 'event_description', $itemid); // Clean the text so no nasties get through. $this->_description = clean_text($this->_description, $this->properties->format); } // Finally return the description. return $this->_description; } /** * Return the number of repeat events there are in this events series. * * @return int number of event repeated */ public function count_repeats() { global $DB; if (!empty($this->properties->repeatid)) { $this->properties->eventrepeats = $DB->count_records('event', array('repeatid' => $this->properties->repeatid)); // We don't want to count ourselves. $this->properties->eventrepeats--; } return $this->properties->eventrepeats; } /** * Update or create an event within the database * * Pass in a object containing the event properties and this function will * insert it into the database and deal with any associated files * * Capability checking should be performed if the user is directly manipulating the event * and no other capability has been tested. However if the event is not being manipulated * directly by the user and another capability has been checked for them to do this then * capabilites should not be checked. * * For example if a user is editing an event in the calendar the check should be true, * but if you are updating an event in an activities settings are changed then the calendar * capabilites should not be checked. * * @see self::create() * @see self::update() * * @param \stdClass $data object of event * @param bool $checkcapability If Moodle should check the user can manage the calendar events for this call or not. * @return bool event updated */ public function update($data, $checkcapability=true) { global $DB, $USER; foreach ($data as $key => $value) { $this->properties->$key = $value; } $this->properties->timemodified = time(); $usingeditor = (!empty($this->properties->description) && is_array($this->properties->description)); // Prepare event data. $eventargs = array( 'context' => $this->get_context(), 'objectid' => $this->properties->id, 'other' => array( 'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid, 'timestart' => $this->properties->timestart, 'name' => $this->properties->name ) ); if (empty($this->properties->id) || $this->properties->id < 1) { if ($checkcapability) { if (!calendar_add_event_allowed($this->properties)) { throw new \moodle_exception('nopermissiontoupdatecalendar'); } } if ($usingeditor) { switch ($this->properties->eventtype) { case 'user': $this->properties->courseid = 0; $this->properties->course = 0; $this->properties->groupid = 0; $this->properties->userid = $USER->id; break; case 'site': $this->properties->courseid = SITEID; $this->properties->course = SITEID; $this->properties->groupid = 0; $this->properties->userid = $USER->id; break; case 'course': $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; default: // We should NEVER get here, but just incase we do lets fail gracefully. $usingeditor = false; break; } // If we are actually using the editor, we recalculate the context because some default values // were set when calculate_context() was called from the constructor. if ($usingeditor) { $this->properties->context = $this->calculate_context(); $this->editorcontext = $this->get_context(); } $editor = $this->properties->description; $this->properties->format = $this->properties->description['format']; $this->properties->description = $this->properties->description['text']; } // Insert the event into the database. $this->properties->id = $DB->insert_record('event', $this->properties); if ($usingeditor) { $this->properties->description = file_save_draft_area_files( $editor['itemid'], $this->editorcontext->id, 'calendar', 'event_description', $this->properties->id, $this->editoroptions, $editor['text'], $this->editoroptions['forcehttps']); $DB->set_field('event', 'description', $this->properties->description, array('id' => $this->properties->id)); } // Log the event entry. $eventargs['objectid'] = $this->properties->id; $eventargs['context'] = $this->get_context(); $event = \core\event\calendar_event_created::create($eventargs); $event->trigger(); $repeatedids = array(); if (!empty($this->properties->repeat)) { $this->properties->repeatid = $this->properties->id; $DB->set_field('event', 'repeatid', $this->properties->repeatid, array('id' => $this->properties->id)); $eventcopy = clone($this->properties); unset($eventcopy->id); $timestart = new \DateTime('@' . $eventcopy->timestart); $timestart->setTimezone(\core_date::get_user_timezone_object()); for ($i = 1; $i < $eventcopy->repeats; $i++) { $timestart->add(new \DateInterval('P7D')); $eventcopy->timestart = $timestart->getTimestamp(); // Get the event id for the log record. $eventcopyid = $DB->insert_record('event', $eventcopy); // If the context has been set delete all associated files. if ($usingeditor) { $fs = get_file_storage(); $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description', $this->properties->id); foreach ($files as $file) { $fs->create_file_from_storedfile(array('itemid' => $eventcopyid), $file); } } $repeatedids[] = $eventcopyid; // Trigger an event. $eventargs['objectid'] = $eventcopyid; $eventargs['other']['timestart'] = $eventcopy->timestart; $event = \core\event\calendar_event_created::create($eventargs); $event->trigger(); } } return true; } else { if ($checkcapability) { if (!calendar_edit_event_allowed($this->properties)) { throw new \moodle_exception('nopermissiontoupdatecalendar'); } } if ($usingeditor) { if ($this->editorcontext !== null) { $this->properties->description = file_save_draft_area_files( $this->properties->description['itemid'], $this->editorcontext->id, 'calendar', 'event_description', $this->properties->id, $this->editoroptions, $this->properties->description['text'], $this->editoroptions['forcehttps']); } else { $this->properties->format = $this->properties->description['format']; $this->properties->description = $this->properties->description['text']; } } $event = $DB->get_record('event', array('id' => $this->properties->id)); $updaterepeated = (!empty($this->properties->repeatid) && !empty($this->properties->repeateditall)); if ($updaterepeated) { $sqlset = 'name = ?, description = ?, timeduration = ?, timemodified = ?, groupid = ?, courseid = ?'; // Note: Group and course id may not be set. If not, keep their current values. $params = [ $this->properties->name, $this->properties->description, $this->properties->timeduration, time(), isset($this->properties->groupid) ? $this->properties->groupid : $event->groupid, isset($this->properties->courseid) ? $this->properties->courseid : $event->courseid, ]; // Note: Only update start date, if it was changed by the user. if ($this->properties->timestart != $event->timestart) { $timestartoffset = $this->properties->timestart - $event->timestart; $sqlset .= ', timestart = timestart + ?'; $params[] = $timestartoffset; } // Note: Only update location, if it was changed by the user. $updatelocation = (!empty($this->properties->location) && $this->properties->location !== $event->location); if ($updatelocation) { $sqlset .= ', location = ?'; $params[] = $this->properties->location; } // Update all. $sql = "UPDATE {event} SET $sqlset WHERE repeatid = ?"; $params[] = $event->repeatid; $DB->execute($sql, $params); // Trigger an update event for each of the calendar event. $events = $DB->get_records('event', array('repeatid' => $event->repeatid), '', '*'); foreach ($events as $calendarevent) { $eventargs['objectid'] = $calendarevent->id; $eventargs['other']['timestart'] = $calendarevent->timestart; $event = \core\event\calendar_event_updated::create($eventargs); $event->add_record_snapshot('event', $calendarevent); $event->trigger(); } } else { $DB->update_record('event', $this->properties); $event = self::load($this->properties->id); $this->properties = $event->properties(); // Trigger an update event. $event = \core\event\calendar_event_updated::create($eventargs); $event->add_record_snapshot('event', $this->properties); $event->trigger(); } return true; } } /** * Deletes an event and if selected an repeated events in the same series * * This function deletes an event, any associated events if $deleterepeated=true, * and cleans up any files associated with the events. * * @see self::delete() * * @param bool $deleterepeated delete event repeatedly * @return bool succession of deleting event */ public function delete($deleterepeated = false) { global $DB; // If $this->properties->id is not set then something is wrong. if (empty($this->properties->id)) { debugging('Attempting to delete an event before it has been loaded', DEBUG_DEVELOPER); return false; } $calevent = $DB->get_record('event', array('id' => $this->properties->id), '*', MUST_EXIST); // Delete the event. $DB->delete_records('event', array('id' => $this->properties->id)); // Trigger an event for the delete action. $eventargs = array( 'context' => $this->get_context(), 'objectid' => $this->properties->id, 'other' => array( 'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid, 'timestart' => $this->properties->timestart, 'name' => $this->properties->name )); $event = \core\event\calendar_event_deleted::create($eventargs); $event->add_record_snapshot('event', $calevent); $event->trigger(); // If we are deleting parent of a repeated event series, promote the next event in the series as parent. if (($this->properties->id == $this->properties->repeatid) && !$deleterepeated) { $newparent = $DB->get_field_sql("SELECT id from {event} where repeatid = ? order by id ASC", array($this->properties->id), IGNORE_MULTIPLE); if (!empty($newparent)) { $DB->execute("UPDATE {event} SET repeatid = ? WHERE repeatid = ?", array($newparent, $this->properties->id)); // Get all records where the repeatid is the same as the event being removed. $events = $DB->get_records('event', array('repeatid' => $newparent)); // For each of the returned events trigger an update event. foreach ($events as $calendarevent) { // Trigger an event for the update. $eventargs['objectid'] = $calendarevent->id; $eventargs['other']['timestart'] = $calendarevent->timestart; $event = \core\event\calendar_event_updated::create($eventargs); $event->add_record_snapshot('event', $calendarevent); $event->trigger(); } } } // If the editor context hasn't already been set then set it now. if ($this->editorcontext === null) { $this->editorcontext = $this->get_context(); } // If the context has been set delete all associated files. if ($this->editorcontext !== null) { $fs = get_file_storage(); $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description', $this->properties->id); foreach ($files as $file) { $file->delete(); } } // If we need to delete repeated events then we will fetch them all and delete one by one. if ($deleterepeated && !empty($this->properties->repeatid) && $this->properties->repeatid > 0) { // Get all records where the repeatid is the same as the event being removed. $events = $DB->get_records('event', array('repeatid' => $this->properties->repeatid)); // For each of the returned events populate an event object and call delete. // make sure the arg passed is false as we are already deleting all repeats. foreach ($events as $event) { $event = new calendar_event($event); $event->delete(false); } } return true; } /** * Fetch all event properties. * * This function returns all of the events properties as an object and optionally * can prepare an editor for the description field at the same time. This is * designed to work when the properties are going to be used to set the default * values of a moodle forms form. * * @param bool $prepareeditor If set to true a editor is prepared for use with * the mforms editor element. (for description) * @return \stdClass Object containing event properties */ public function properties($prepareeditor = false) { global $DB; // First take a copy of the properties. We don't want to actually change the // properties or we'd forever be converting back and forwards between an // editor formatted description and not. $properties = clone($this->properties); // Clean the description here. $properties->description = clean_text($properties->description, $properties->format); // If set to true we need to prepare the properties for use with an editor // and prepare the file area. if ($prepareeditor) { // We may or may not have a property id. If we do then we need to work // out the context so we can copy the existing files to the draft area. if (!empty($properties->id)) { if ($properties->eventtype === 'site') { // Site context. $this->editorcontext = $this->get_context(); } else if ($properties->eventtype === 'user') { // User context. $this->editorcontext = $this->get_context(); } else if ($properties->eventtype === 'group' || $properties->eventtype === 'course') { // First check the course is valid. $course = $DB->get_record('course', array('id' => $properties->courseid)); if (!$course) { throw new \moodle_exception('invalidcourse'); } // Course context. $this->editorcontext = $this->get_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 ($properties->eventtype === 'category') { // First check the course is valid. \core_course_category::get($properties->categoryid, MUST_EXIST, true); // Course context. $this->editorcontext = $this->get_context(); } 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 // code and we won't need the editor. $this->editoroptions['maxbytes'] = 0; $this->editoroptions['maxfiles'] = 0; } if (empty($this->editorcontext) || empty($this->editorcontext->id)) { $contextid = false; } else { // Get the context id that is what we really want. $contextid = $this->editorcontext->id; } } else { // If we get here then this is a new event in which case we don't need a // context as there is no existing files to copy to the draft area. $contextid = null; } // If the contextid === false we don't support files so no preparing // a draft area. if ($contextid !== false) { // Just encase it has already been submitted. $draftiddescription = file_get_submitted_draft_itemid('description'); // Prepare the draft area, this copies existing files to the draft area as well. $properties->description = file_prepare_draft_area($draftiddescription, $contextid, 'calendar', 'event_description', $properties->id, $this->editoroptions, $properties->description); } else { $draftiddescription = 0; } // Structure the description field as the editor requires. $properties->description = array('text' => $properties->description, 'format' => $properties->format, 'itemid' => $draftiddescription); } // Finally return the properties. return $properties; } /** * Toggles the visibility of an event * * @param null|bool $force If it is left null the events visibility is flipped, * If it is false the event is made hidden, if it is true it * is made visible. * @return bool if event is successfully updated, toggle will be visible */ public function toggle_visibility($force = null) { global $DB; // Set visible to the default if it is not already set. if (empty($this->properties->visible)) { $this->properties->visible = 1; } if ($force === true || ($force !== false && $this->properties->visible == 0)) { // Make this event visible. $this->properties->visible = 1; } else { // Make this event hidden. $this->properties->visible = 0; } // Update the database to reflect this change. $success = $DB->set_field('event', 'visible', $this->properties->visible, array('id' => $this->properties->id)); $calendarevent = $DB->get_record('event', array('id' => $this->properties->id), '*', MUST_EXIST); // Prepare event data. $eventargs = array( 'context' => $this->get_context(), 'objectid' => $this->properties->id, 'other' => array( 'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid, 'timestart' => $this->properties->timestart, 'name' => $this->properties->name ) ); $event = \core\event\calendar_event_updated::create($eventargs); $event->add_record_snapshot('event', $calendarevent); $event->trigger(); return $success; } /** * Returns an event object when provided with an event id. * * This function makes use of MUST_EXIST, if the event id passed in is invalid * it will result in an exception being thrown. * * @param int|object $param event object or event id * @return calendar_event */ public static function load($param) { global $DB; if (is_object($param)) { $event = new calendar_event($param); } else { $event = $DB->get_record('event', array('id' => (int)$param), '*', MUST_EXIST); $event = new calendar_event($event); } return $event; } /** * Creates a new event and returns an event object. * * Capability checking should be performed if the user is directly creating the event * and no other capability has been tested. However if the event is not being created * directly by the user and another capability has been checked for them to do this then * capabilites should not be checked. * * For example if a user is creating an event in the calendar the check should be true, * but if you are creating an event in an activity when it is created then the calendar * capabilites should not be checked. * * @param \stdClass|array $properties An object containing event properties * @param bool $checkcapability If Moodle should check the user can manage the calendar events for this call or not. * @throws \coding_exception * * @return calendar_event|bool The event object or false if it failed */ public static function create($properties, $checkcapability = true) { if (is_array($properties)) { $properties = (object)$properties; } if (!is_object($properties)) { throw new \coding_exception('When creating an event properties should be either an object or an assoc array'); } $event = new calendar_event($properties); if ($event->update($properties, $checkcapability)) { return $event; } else { return false; } } /** * Format the event name using the external API. * * This function should we used when text formatting is required in external functions. * * @return string Formatted name. */ public function format_external_name() { if ($this->editorcontext === null) { // Switch on the event type to decide upon the appropriate context to use for this event. $this->editorcontext = $this->get_context(); } return \core_external\util::format_string($this->properties->name, $this->editorcontext->id); } /** * Format the text using the external API. * * This function should we used when text formatting is required in external functions. * * @return array an array containing the text formatted and the text format */ public function format_external_text() { if ($this->editorcontext === null) { // Switch on the event type to decide upon the appropriate context to use for this event. $this->editorcontext = $this->get_context(); if (!calendar_is_valid_eventtype($this->properties->eventtype)) { // We don't have a context here, do a normal format_text. return \core_external\util::format_text( $this->properties->description, $this->properties->format, $this->editorcontext ); } } // Work out the item id for the editor, if this is a repeated event then the files will be associated with the original. if (!empty($this->properties->repeatid) && $this->properties->repeatid > 0) { $itemid = $this->properties->repeatid; } else { $itemid = $this->properties->id; } return \core_external\util::format_text( $this->properties->description, $this->properties->format, $this->editorcontext, 'calendar', 'event_description', $itemid ); } } /** * Calendar information class * * This class is used simply to organise the information pertaining to a calendar * and is used primarily to make information easily available. * * @package core_calendar * @category calendar * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class calendar_information { /** * @var int The timestamp * * Rather than setting the day, month and year we will set a timestamp which will be able * to be used by multiple calendars. */ public $time; /** @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(); /** @var array An array of groups */ public $groups = array(); /** @var array An array of users */ public $users = array(); /** @var context The anticipated context that the calendar is viewed in */ public $context = null; /** @var string The calendar's view mode. */ protected $viewmode; /** @var \stdClass course data. */ public $course; /** @var int day. */ protected $day; /** @var int month. */ protected $month; /** @var int year. */ protected $year; /** * Creates a new instance * * @param int $day the number of the day * @param int $month the number of the month * @param int $year the number of the year * @param int $time the unixtimestamp representing the date we want to view, this is used instead of $calmonth * and $calyear to support multiple calendars */ public function __construct($day = 0, $month = 0, $year = 0, $time = 0) { // 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($day) || !empty($month) || !empty($year)) { $date = usergetdate(time()); if (empty($day)) { $day = $date['mday']; } if (empty($month)) { $month = $date['mon']; } if (empty($year)) { $year = $date['year']; } if (checkdate($month, $day, $year)) { $time = make_timestamp($year, $month, $day); } else { $time = time(); } } $this->set_time($time); } /** * Creates and set up a instance. * * @param int $time the unixtimestamp representing the date we want to view. * @param int $courseid The ID of the course the user wishes to view. * @param int $categoryid The ID of the category the user wishes to view * If a courseid is specified, this value is ignored. * @return calendar_information */ public static function create($time, int $courseid, ?int $categoryid = null): calendar_information { $calendar = new static(0, 0, 0, $time); if ($courseid != SITEID && !empty($courseid)) { // Course ID must be valid and existing. $course = get_course($courseid); $calendar->context = context_course::instance($course->id); if (!$course->visible && !is_role_switched($course->id)) { require_capability('moodle/course:viewhiddencourses', $calendar->context); } $courses = [$course->id => $course]; $category = (\core_course_category::get($course->category, MUST_EXIST, true))->get_db_record(); } else if (!empty($categoryid)) { $course = get_site(); $courses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce'); // Filter available courses to those within this category or it's children. $ids = [$categoryid]; $category = \core_course_category::get($categoryid); $ids = array_merge($ids, array_keys($category->get_children())); $courses = array_filter($courses, function($course) use ($ids) { return array_search($course->category, $ids) !== false; }); $category = $category->get_db_record(); $calendar->context = context_coursecat::instance($categoryid); } else { $course = get_site(); $courses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce'); $category = null; $calendar->context = context_system::instance(); } $calendar->set_sources($course, $courses, $category); return $calendar; } /** * Set the time period of this instance. * * @param int $time the unixtimestamp representing the date we want to view. * @return $this */ public function set_time($time = null) { if (empty($time)) { $this->time = time(); } else { $this->time = $time; } return $this; } /** * @deprecated 3.4 */ #[\core\attribute\deprecated('prepare_for_view', since: '3.4', mdl: 'MDL-59890', final: true)] public function prepare_for_view() { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } /** * 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 stdClass[] $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) { global $USER; // A cousre must always be specified. $this->course = $course; $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; // Determine the correct category information to show. // When called with a course, the category of that course is usually included too. // When a category was specifically requested, it should be requested with the site id. if (SITEID !== $this->courseid) { // A specific course was requested. // Fetch the category that this course is in, along with all parents. // Do not include child categories of this category, as the user many not have enrolments in those siblings or children. $category = \core_course_category::get($course->category, MUST_EXIST, true); $this->categoryid = $category->id; $this->categories = $category->get_parents(); $this->categories[] = $category->id; } else if (null !== $category && $category->id > 0) { // A specific category was requested. // Fetch all parents of this category, along with all children too. $category = \core_course_category::get($category->id); $this->categoryid = $category->id; // Build the category list. // This includes the current category. $this->categories = $category->get_parents(); $this->categories[] = $category->id; $this->categories = array_merge($this->categories, $category->get_all_children_ids()); } else if (SITEID === $this->courseid) { // The site was requested. // Fetch all categories where this user has any enrolment, and all categories that this user can manage. // Grab the list of categories that this user has courses in. $coursecategories = array_flip(array_map(function($course) { return $course->category; }, $courses)); $calcatcache = cache::make('core', 'calendar_categories'); $this->categories = $calcatcache->get('site'); if ($this->categories === false) { // Use the category id as the key in the following array. That way we do not have to remove duplicates. $categories = []; foreach (\core_course_category::get_all() as $category) { if (isset($coursecategories[$category->id]) || has_capability('moodle/category:manage', $category->get_context(), $USER, false)) { // If the user has access to a course in this category or can manage the category, // then they can see all parent categories too. $categories[$category->id] = true; foreach ($category->get_parents() as $catid) { $categories[$catid] = true; } } } $this->categories = array_keys($categories); $calcatcache->set('site', $this->categories); } } } /** * Ensures the date for the calendar is correct and either sets it to now * or throws a moodle_exception if not * * @param bool $defaultonow use current time * @throws moodle_exception * @return bool validation of checkdate */ public function checkdate($defaultonow = true) { if (!checkdate($this->month, $this->day, $this->year)) { if ($defaultonow) { $now = usergetdate(time()); $this->day = intval($now['mday']); $this->month = intval($now['mon']); $this->year = intval($now['year']); return true; } else { throw new moodle_exception('invaliddate'); } } return true; } /** * Gets todays timestamp for the calendar * * @return int today timestamp */ public function timestamp_today() { return $this->time; } /** * Gets tomorrows timestamp for the calendar * * @return int tomorrow timestamp */ public function timestamp_tomorrow() { return strtotime('+1 day', $this->time); } /** * Adds the pretend blocks for the calendar * * @param core_calendar_renderer $renderer * @param bool $showfilters display filters, false is set as default * @param string|null $view preference view options (eg: day, month, upcoming) */ public function add_sidecalendar_blocks(core_calendar_renderer $renderer, $showfilters=false, $view=null) { global $PAGE; if (!has_capability('moodle/block:view', $PAGE->context) ) { return; } if ($showfilters) { $filters = new block_contents(); $filters->content = $renderer->event_filter(); $filters->footer = ''; $filters->title = get_string('eventskey', 'calendar'); $renderer->add_pretend_calendar_block($filters, BLOCK_POS_RIGHT); } } /** * Getter method for the calendar's view mode. * * @return string */ public function get_viewmode(): string { return $this->viewmode; } /** * Setter method for the calendar's view mode. * * @param string $viewmode */ public function set_viewmode(string $viewmode): void { $this->viewmode = $viewmode; } } /** * Get calendar events. * * @param int $tstart Start time of time range for events * @param int $tend End time of time range for events * @param array|int|boolean $users array of users, user id or boolean for all/no user events * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events * @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, $categories = []) { global $DB; $whereclause = ''; $params = array(); // Quick test. 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 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 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 AND e.categoryid = 0)'; } else if($users === false) { // No user at all, do nothing } if ((is_array($groups) && !empty($groups)) or is_numeric($groups)) { // Events from a number of groups if(!empty($whereclause)) $whereclause .= ' OR'; list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); $whereclause .= " e.groupid $insqlgroups "; $params = array_merge($params, $inparamsgroups); } else if($groups === true) { // Events from ALL groups if(!empty($whereclause)) $whereclause .= ' OR '; $whereclause .= ' e.groupid != 0'; } // boolean false (no groups at all): we don't need to do anything if ((is_array($courses) && !empty($courses)) or is_numeric($courses)) { if(!empty($whereclause)) $whereclause .= ' OR'; list($insqlcourses, $inparamscourses) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED); $whereclause .= " (e.groupid = 0 AND e.courseid $insqlcourses)"; $params = array_merge($params, $inparamscourses); } else if ($courses === true) { // Events from ALL courses if(!empty($whereclause)) $whereclause .= ' OR'; $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 // valid query with only time constraints, thus selecting ALL events in that time frame! if(empty($whereclause)) { return array(); } if($withduration) { $timeclause = '(e.timestart >= '.$tstart.' OR e.timestart + e.timeduration > '.$tstart.') AND e.timestart <= '.$tend; } else { $timeclause = 'e.timestart >= '.$tstart.' AND e.timestart <= '.$tend; } if(!empty($whereclause)) { // We have additional constraints $whereclause = $timeclause.' AND ('.$whereclause.')'; } else { // Just basic time filtering $whereclause = $timeclause; } if ($ignorehidden) { $whereclause .= ' AND e.visible = 1'; } $sql = "SELECT e.* FROM {event} e LEFT JOIN {modules} m ON e.modulename = m.name -- Non visible modules will have a value of 0. WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause ORDER BY e.timestart"; $events = $DB->get_records_sql($sql, $params); if ($events === false) { $events = array(); } return $events; } /** * Return the days of the week. * * @return array array of days */ function calendar_get_days() { $calendartype = \core_calendar\type_factory::get_calendar_instance(); return $calendartype->get_weekdays(); } /** * Get the subscription from a given id. * * @since Moodle 2.5 * @param int $id id of the subscription * @return stdClass Subscription record from DB * @throws moodle_exception for an invalid id */ function calendar_get_subscription($id) { global $DB; $cache = \cache::make('core', 'calendar_subscriptions'); $subscription = $cache->get($id); if (empty($subscription)) { $subscription = $DB->get_record('event_subscriptions', array('id' => $id), '*', MUST_EXIST); $cache->set($id, $subscription); } return $subscription; } /** * Gets the first day of the week. * * Used to be define('CALENDAR_STARTING_WEEKDAY', blah); * * @return int */ function calendar_get_starting_weekday() { $calendartype = \core_calendar\type_factory::get_calendar_instance(); return $calendartype->get_starting_weekday(); } /** * Get current module cache. * * Only use this method if you do not know courseid. Otherwise use: * get_fast_modinfo($courseid)->instances[$modulename][$instance] * * @param array $modulecache in memory module cache * @param string $modulename name of the module * @param int $instance module instance number * @return stdClass|bool $module information */ function calendar_get_module_cached(&$modulecache, $modulename, $instance) { if (!isset($modulecache[$modulename . '_' . $instance])) { $modulecache[$modulename . '_' . $instance] = get_coursemodule_from_instance($modulename, $instance); } return $modulecache[$modulename . '_' . $instance]; } /** * Get current course cache. * * @param array $coursecache list of course cache * @param int $courseid id of the course * @return stdClass $coursecache[$courseid] return the specific course cache */ function calendar_get_course_cached(&$coursecache, $courseid) { if (!isset($coursecache[$courseid])) { $coursecache[$courseid] = get_course($courseid); } return $coursecache[$courseid]; } /** * Get group from groupid for calendar display * * @param int $groupid * @return stdClass group object with fields 'id', 'name' and 'courseid' */ function calendar_get_group_cached($groupid) { static $groupscache = array(); if (!isset($groupscache[$groupid])) { $groupscache[$groupid] = groups_get_group($groupid, 'id,name,courseid'); } return $groupscache[$groupid]; } /** * Get calendar events by id. * * @since Moodle 2.5 * @param array $eventids list of event ids * @return array Array of event entries, empty array if nothing found */ function calendar_get_events_by_id($eventids) { global $DB; if (!is_array($eventids) || empty($eventids)) { return array(); } list($wheresql, $params) = $DB->get_in_or_equal($eventids); $wheresql = "id $wheresql"; return $DB->get_records_select('event', $wheresql, $params); } /** * Adds day, month, year arguments to a URL and returns a moodle_url object. * * @param string|moodle_url $linkbase * @param int $d The number of the day. * @param int $m The number of the month. * @param int $y The number of the year. * @param int $time the unixtime, used for multiple calendar support. The values $d, * $m and $y are kept for backwards compatibility. * @return moodle_url|null $linkbase */ function calendar_get_link_href($linkbase, $d, $m, $y, $time = 0) { if (empty($linkbase)) { return null; } if (!($linkbase instanceof \moodle_url)) { $linkbase = new \moodle_url($linkbase); } $linkbase->param('time', calendar_get_timestamp($d, $m, $y, $time)); return $linkbase; } /** * Return the number of days in month. * * @param int $month the number of the month. * @param int $year the number of the year * @return int */ function calendar_days_in_month($month, $year) { $calendartype = \core_calendar\type_factory::get_calendar_instance(); return $calendartype->get_num_days_in_month($year, $month); } /** * Returns the courses to load events for. * * @param array $courseeventsfrom An array of courses to load calendar events for * @param bool $ignorefilters specify the use of filters, false is set as default * @param stdClass $user The user object. This defaults to the global $USER object. * @return array An array of courses, groups, and user to load calendar events for based upon filters */ function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false, ?stdClass $user = null) { global $CFG, $USER; if (is_null($user)) { $user = $USER; } $courses = array(); $userid = false; $group = false; // Get the capabilities that allow seeing group events from all groups. $allgroupscaps = array('moodle/site:accessallgroups', 'moodle/calendar:manageentries'); $isvaliduser = !empty($user->id); if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_COURSE, $user)) { $courses = array_keys($courseeventsfrom); } if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_SITE, $user)) { $courses[] = SITEID; } $courses = array_unique($courses); sort($courses); if (!empty($courses) && in_array(SITEID, $courses)) { // Sort courses for consistent colour highlighting. // Effectively ignoring SITEID as setting as last course id. $key = array_search(SITEID, $courses); unset($courses[$key]); $courses[] = SITEID; } if ($ignorefilters || ($isvaliduser && calendar_show_event_type(CALENDAR_EVENT_USER, $user))) { $userid = $user->id; } if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP, $user) || $ignorefilters)) { if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, \context_system::instance())) { $group = true; } else if ($isvaliduser) { $groupids = []; foreach ($courseeventsfrom as $courseid => $course) { if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) { if (has_all_capabilities($allgroupscaps, \context_course::instance($courseid))) { // User can access all groups in this course. // Get all the groups in this course. $coursegroups = groups_get_all_groups($course->id, 0, 0, 'g.id'); $groupids = array_merge($groupids, array_keys($coursegroups)); } else { // User can only access their own groups. // Get the groups the user is in. $coursegroups = groups_get_user_groups($course->id, $user->id); $groupids = array_merge($groupids, $coursegroups['0']); } } } if (!empty($groupids)) { $group = $groupids; } } } if (empty($courses)) { $courses = false; } return array($courses, $group, $userid); } /** * Can current user manage a non user event in system context. * * @param calendar_event|stdClass $event event object * @return boolean */ function calendar_can_manage_non_user_event_in_system($event) { $sitecontext = \context_system::instance(); $isuserevent = $event->eventtype == 'user'; $canmanageentries = has_capability('moodle/calendar:manageentries', $sitecontext); // If user has manageentries at site level and it's not user event, return true. if ($canmanageentries && !$isuserevent) { return true; } return false; } /** * Return the capability for viewing a calendar event. * * @param calendar_event $event event object * @return boolean */ function calendar_view_event_allowed(calendar_event $event) { global $USER; // Anyone can see site events. if ($event->courseid && $event->courseid == SITEID) { return true; } if (calendar_can_manage_non_user_event_in_system($event)) { return true; } if (!empty($event->groupid)) { // If it is a group event we need to be able to manage events in the course, or be in the group. if (has_capability('moodle/calendar:manageentries', $event->context) || has_capability('moodle/calendar:managegroupentries', $event->context)) { return true; } $mycourses = enrol_get_my_courses('id'); return isset($mycourses[$event->courseid]) && groups_is_member($event->groupid); } else if ($event->modulename) { // If this is a module event we need to be able to see the module. $coursemodules = []; $courseid = 0; // Override events do not have the courseid set. if ($event->courseid) { $courseid = $event->courseid; $coursemodules = get_fast_modinfo($event->courseid)->instances; } else { $cmraw = get_coursemodule_from_instance($event->modulename, $event->instance, 0, false, MUST_EXIST); $courseid = $cmraw->course; $coursemodules = get_fast_modinfo($cmraw->course)->instances; } $hasmodule = isset($coursemodules[$event->modulename]); $hasinstance = isset($coursemodules[$event->modulename][$event->instance]); // If modinfo doesn't know about the module, return false to be safe. if (!$hasmodule || !$hasinstance) { return false; } // Must be able to see the course and the module - MDL-59304. $cm = $coursemodules[$event->modulename][$event->instance]; if (!$cm->uservisible) { return false; } $mycourses = enrol_get_my_courses('id'); return isset($mycourses[$courseid]); } else if ($event->categoryid) { // If this is a category we need to be able to see the category. $cat = \core_course_category::get($event->categoryid, IGNORE_MISSING); if (!$cat) { return false; } return true; } else if (!empty($event->courseid)) { // If it is a course event we need to be able to manage events in the course, or be in the course. if (has_capability('moodle/calendar:manageentries', $event->context)) { return true; } return can_access_course(get_course($event->courseid)); } else if ($event->userid) { return calendar_can_manage_user_event($event); } else { throw new moodle_exception('unknown event type'); } return false; } /** * Return the capability for editing calendar event. * * @param calendar_event $event event object * @param bool $manualedit is the event being edited manually by the user * @return bool capability to edit event */ function calendar_edit_event_allowed($event, $manualedit = false) { global $USER, $DB; // Must be logged in. if (!isloggedin()) { return false; } // Can not be using guest account. if (isguestuser()) { return false; } if ($manualedit && !empty($event->modulename)) { $hascallback = component_callback_exists( 'mod_' . $event->modulename, 'core_calendar_event_timestart_updated' ); if (!$hascallback) { // If the activity hasn't implemented the correct callback // to handle changes to it's events then don't allow any // manual changes to them. return false; } $coursemodules = get_fast_modinfo($event->courseid)->instances; $hasmodule = isset($coursemodules[$event->modulename]); $hasinstance = isset($coursemodules[$event->modulename][$event->instance]); // If modinfo doesn't know about the module, return false to be safe. if (!$hasmodule || !$hasinstance) { return false; } $coursemodule = $coursemodules[$event->modulename][$event->instance]; $context = context_module::instance($coursemodule->id); // This is the capability that allows a user to modify the activity // settings. Since the activity generated this event we need to check // that the current user has the same capability before allowing them // to update the event because the changes to the event will be // reflected within the activity. return has_capability('moodle/course:manageactivities', $context); } if ($manualedit && !empty($event->component)) { // TODO possibly we can later add a callback similar to core_calendar_event_timestart_updated in the modules. return false; } // You cannot edit URL based calendar subscription events presently. if (!empty($event->subscriptionid)) { if (!empty($event->subscription->url)) { // This event can be updated externally, so it cannot be edited. return false; } } if (calendar_can_manage_non_user_event_in_system($event)) { return true; } // If groupid is set, it's definitely a group event. if (!empty($event->groupid)) { // Allow users to add/edit group events if - // 1) They have manageentries for the course OR // 2) They have managegroupentries AND are in the group. $group = $DB->get_record('groups', array('id' => $event->groupid)); return $group && ( has_capability('moodle/calendar:manageentries', $event->context) || (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 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. return (has_capability('moodle/calendar:manageownentries', context_user::instance($event->userid))); } else if (!empty($event->userid)) { return calendar_can_manage_user_event($event); } return false; } /** * Can current user edit/delete/add an user event? * * @param calendar_event|stdClass $event event object * @return bool */ function calendar_can_manage_user_event($event): bool { global $USER; if (!($event instanceof \calendar_event)) { $event = new \calendar_event(clone($event)); } $canmanage = has_capability('moodle/calendar:manageentries', $event->context); $canmanageown = has_capability('moodle/calendar:manageownentries', $event->context); $ismyevent = $event->userid == $USER->id; $isadminevent = is_siteadmin($event->userid); if ($canmanageown && $ismyevent) { return true; } // In site context, user must have login and calendar:manageentries permissions // ... to manage other user's events except admin users. if ($canmanage && !$isadminevent) { return true; } return false; } /** * Return the capability for deleting a calendar event. * * @param calendar_event $event The event object * @return bool Whether the user has permission to delete the event or not. */ function calendar_delete_event_allowed($event) { // Only allow delete if you have capabilities and it is not an module or component event. return (calendar_edit_event_allowed($event) && empty($event->modulename) && empty($event->component)); } /** * Returns the default courses to display on the calendar when there isn't a specific * course to display. * * @param int $courseid (optional) If passed, an additional course can be returned for admins (the current course). * @param string $fields Comma separated list of course fields to return. * @param bool $canmanage If true, this will return the list of courses the user can create events in, rather * than the list of courses they see events from (an admin can always add events in a course * calendar, even if they are not enrolled in the course). * @param int $userid (optional) The user which this function returns the default courses for. * By default the current user. * @return array $courses Array of courses to display */ function calendar_get_default_courses($courseid = null, $fields = '*', $canmanage = false, ?int $userid = null) { global $CFG, $USER; if (!$userid) { if (!isloggedin()) { return array(); } $userid = $USER->id; } if ((!empty($CFG->calendar_adminseesall) || $canmanage) && has_capability('moodle/calendar:manageentries', context_system::instance(), $userid)) { // Add a c. prefix to every field as expected by get_courses function. $fieldlist = explode(',', $fields); $prefixedfields = array_map(function($value) { return 'c.' . trim(strtolower($value)); }, $fieldlist); $courses = get_courses('all', 'c.shortname', implode(',', $prefixedfields)); } else { $courses = enrol_get_users_courses($userid, true, $fields, 'c.shortname'); } if ($courseid && $courseid != SITEID) { if (empty($courses[$courseid]) && has_capability('moodle/calendar:manageentries', context_system::instance(), $userid)) { // Allow a site admin to see calendars from courses he is not enrolled in. // This will come from $COURSE. $courses[$courseid] = get_course($courseid); } } return $courses; } /** * Format event location property * * @param calendar_event $event * @return string */ function calendar_format_event_location(calendar_event $event): string { $location = format_text($event->location, FORMAT_PLAIN, ['context' => $event->context]); // If it looks like a link, convert it to one. if (preg_match('/^https?:\/\//i', $location) && clean_param($location, PARAM_URL)) { $location = \html_writer::link($location, $location, [ 'title' => get_string('eventnamelocation', 'core_calendar', ['name' => $event->name, 'location' => $location]), ]); } return $location; } /** * Checks to see if the requested type of event should be shown for the given user. * * @param int $type The type to check the display for (default is to display all) * @param stdClass|int|null $user The user to check for - by default the current user * @return bool True if the tyep should be displayed false otherwise */ function calendar_show_event_type($type, $user = null) { $default = CALENDAR_EVENT_SITE + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER; if ((int)get_user_preferences('calendar_persistflt', 0, $user) === 0) { global $SESSION; if (!isset($SESSION->calendarshoweventtype)) { $SESSION->calendarshoweventtype = $default; } return $SESSION->calendarshoweventtype & $type; } else { return get_user_preferences('calendar_savedflt', $default, $user) & $type; } } /** * Sets the display of the event type given $display. * * If $display = true the event type will be shown. * If $display = false the event type will NOT be shown. * If $display = null the current value will be toggled and saved. * * @param int $type object of CALENDAR_EVENT_XXX * @param bool $display option to display event type * @param stdClass|int $user moodle user object or id, null means current user */ function calendar_set_event_type_display($type, $display = null, $user = null) { $persist = (int)get_user_preferences('calendar_persistflt', 0, $user); $default = CALENDAR_EVENT_SITE + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER + CALENDAR_EVENT_COURSECAT; if ($persist === 0) { global $SESSION; if (!isset($SESSION->calendarshoweventtype)) { $SESSION->calendarshoweventtype = $default; } $preference = $SESSION->calendarshoweventtype; } else { $preference = get_user_preferences('calendar_savedflt', $default, $user); } $current = $preference & $type; if ($display === null) { $display = !$current; } if ($display && !$current) { $preference += $type; } else if (!$display && $current) { $preference -= $type; } if ($persist === 0) { $SESSION->calendarshoweventtype = $preference; } else { if ($preference == $default) { unset_user_preference('calendar_savedflt', $user); } else { set_user_preference('calendar_savedflt', $preference, $user); } } } /** * Get calendar's allowed types. * * @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, $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 (has_capability('moodle/site:accessallgroups', $context)) { return is_null($groups) ? groups_get_all_groups($course->id) : $groups; } else { if (is_null($groups)) { return groups_get_all_groups($course->id, $user->id); } else { return array_filter($groups, function($group) use ($user) { return isset($group->members[$user->id]); }); } } } return false; }; if (!empty($course)) { if (!is_object($course)) { $course = $DB->get_record('course', array('id' => $course), 'id, groupmode, groupmodeforce', MUST_EXIST); } if ($course->id != SITEID) { $coursecontext = \context_course::instance($course->id); $allowed->user = has_capability('moodle/calendar:manageownentries', $coursecontext); if (has_capability('moodle/calendar:manageentries', $coursecontext)) { $allowed->courses = array($course->id => 1); $allowed->groups = $getgroupsfunc($course, $coursecontext, $USER); } else if (has_capability('moodle/calendar:managegroupentries', $coursecontext)) { $allowed->groups = $getgroupsfunc($course, $coursecontext, $USER); } } } if (!empty($category)) { $catcontext = \context_coursecat::instance($category->id); if (has_capability('moodle/category:manage', $catcontext)) { $allowed->categories = [$category->id => 1]; } } } /** * See if user can add calendar entries at all used to print the "New Event" button. * * @param stdClass $course object of a course or course id * @return bool has the capability to add at least one event type */ function calendar_user_can_add_event($course) { if (!isloggedin() || isguestuser()) { return false; } calendar_get_allowed_types($allowed, $course); return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->categories || $allowed->site); } /** * Check wether the current user is permitted to add events. * * @param stdClass $event object of event * @return bool has the capability to add event */ function calendar_add_event_allowed($event) { global $USER, $DB; // Can not be using guest account. if (!isloggedin() or isguestuser()) { return false; } if (calendar_can_manage_non_user_event_in_system($event)) { return true; } 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': // Allow users to add/edit group events if - // 1) They have manageentries (= entries for whole course). // 2) They have managegroupentries AND are in the group. $group = $DB->get_record('groups', array('id' => $event->groupid)); return $group && ( has_capability('moodle/calendar:manageentries', $event->context) || (has_capability('moodle/calendar:managegroupentries', $event->context) && groups_is_member($event->groupid))); case 'user': return calendar_can_manage_user_event($event); case 'site': return has_capability('moodle/calendar:manageentries', $event->context); default: return has_capability('moodle/calendar:manageentries', $event->context); } } /** * Returns option list for the poll interval setting. * * @return array An array of poll interval options. Interval => description. */ function calendar_get_pollinterval_choices() { return array( '0' => get_string('never', 'calendar'), HOURSECS => get_string('hourly', 'calendar'), DAYSECS => get_string('daily', 'calendar'), WEEKSECS => get_string('weekly', 'calendar'), '2628000' => get_string('monthly', 'calendar'), YEARSECS => get_string('annually', 'calendar') ); } /** * Returns option list of available options for the calendar event type, given the current user and course. * * @param int $courseid The id of the course * @return array An array containing the event types the user can create. */ function calendar_get_eventtype_choices($courseid) { $choices = array(); $allowed = new \stdClass; calendar_get_allowed_types($allowed, $courseid); if ($allowed->user) { $choices['user'] = get_string('userevents', 'calendar'); } if ($allowed->site) { $choices['site'] = get_string('siteevents', 'calendar'); } 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'); } return array($choices, $allowed->groups); } /** * Add an iCalendar subscription to the database. * * @param stdClass $sub The subscription object (e.g. from the form) * @return int The insert ID, if any. */ function calendar_add_subscription($sub) { global $DB, $USER, $SITE; // Undo the form definition work around to allow us to have two different // course selectors present depending on which event type the user selects. if (!empty($sub->groupcourseid)) { $sub->courseid = $sub->groupcourseid; unset($sub->groupcourseid); } // Default course id if none is set. if (empty($sub->courseid)) { if ($sub->eventtype === 'site') { $sub->courseid = SITEID; } else { $sub->courseid = 0; } } if ($sub->eventtype === 'site') { $sub->courseid = $SITE->id; } else if ($sub->eventtype === 'group' || $sub->eventtype === 'course') { $sub->courseid = $sub->courseid; } else if ($sub->eventtype === 'category') { $sub->categoryid = $sub->categoryid; } else { // User events. $sub->courseid = 0; } $sub->userid = $USER->id; // File subscriptions never update. if (empty($sub->url)) { $sub->pollinterval = 0; } if (!empty($sub->name)) { if (empty($sub->id)) { $id = $DB->insert_record('event_subscriptions', $sub); // We cannot cache the data here because $sub is not complete. $sub->id = $id; // Trigger event, calendar subscription added. $eventparams = array('objectid' => $sub->id, 'context' => calendar_get_calendar_context($sub), 'other' => array( 'eventtype' => $sub->eventtype, ) ); switch ($sub->eventtype) { case 'category': $eventparams['other']['categoryid'] = $sub->categoryid; break; case 'course': $eventparams['other']['courseid'] = $sub->courseid; break; case 'group': $eventparams['other']['courseid'] = $sub->courseid; $eventparams['other']['groupid'] = $sub->groupid; break; default: $eventparams['other']['courseid'] = $sub->courseid; } $event = \core\event\calendar_subscription_created::create($eventparams); $event->trigger(); return $id; } else { // Why are we doing an update here? calendar_update_subscription($sub); return $sub->id; } } else { throw new \moodle_exception('errorbadsubscription', 'importcalendar'); } } /** * Add an iCalendar event to the Moodle calendar. * * @param stdClass $event The RFC-2445 iCalendar event * @param int $unused Deprecated * @param int $subscriptionid The iCalendar subscription ID * @param string $timezone The X-WR-TIMEZONE iCalendar property if provided * @throws dml_exception A DML specific exception is thrown for invalid subscriptionids. * @return int Code: CALENDAR_IMPORT_EVENT_UPDATED = updated, CALENDAR_IMPORT_EVENT_INSERTED = inserted, 0 = error */ function calendar_add_icalendar_event($event, $unused, $subscriptionid, $timezone='UTC') { global $DB; // Probably an unsupported X-MICROSOFT-CDO-BUSYSTATUS event. if (empty($event->properties['SUMMARY'])) { return 0; } $name = $event->properties['SUMMARY'][0]->value; $name = str_replace('\n', '
', $name); $name = str_replace('\\', '', $name); $name = preg_replace('/\s+/u', ' ', $name); $eventrecord = new \stdClass; $eventrecord->name = clean_param($name, PARAM_NOTAGS); if (empty($event->properties['DESCRIPTION'][0]->value)) { $description = ''; } else { $description = $event->properties['DESCRIPTION'][0]->value; $description = clean_param($description, PARAM_NOTAGS); $description = str_replace('\n', '
', $description); $description = str_replace('\\', '', $description); $description = preg_replace('/\s+/u', ' ', $description); } $eventrecord->description = $description; // Probably a repeating event with RRULE etc. TODO: skip for now. if (empty($event->properties['DTSTART'][0]->value)) { return 0; } if (isset($event->properties['DTSTART'][0]->parameters['TZID'])) { $tz = $event->properties['DTSTART'][0]->parameters['TZID']; } else { $tz = $timezone; } $tz = \core_date::normalise_timezone($tz); $eventrecord->timestart = strtotime($event->properties['DTSTART'][0]->value . ' ' . $tz); if (empty($event->properties['DTEND'])) { $eventrecord->timeduration = 0; // No duration if no end time specified. } else { if (isset($event->properties['DTEND'][0]->parameters['TZID'])) { $endtz = $event->properties['DTEND'][0]->parameters['TZID']; } else { $endtz = $timezone; } $endtz = \core_date::normalise_timezone($endtz); $eventrecord->timeduration = strtotime($event->properties['DTEND'][0]->value . ' ' . $endtz) - $eventrecord->timestart; } // Check to see if it should be treated as an all day event. if ($eventrecord->timeduration == DAYSECS) { // Check to see if the event started at Midnight on the imported calendar. date_default_timezone_set($timezone); if (date('H:i:s', $eventrecord->timestart) === "00:00:00") { // This event should be an all day event. This is not correct, we don't do anything differently for all day events. // See MDL-56227. $eventrecord->timeduration = 0; } \core_date::set_default_server_timezone(); } $eventrecord->location = empty($event->properties['LOCATION'][0]->value) ? '' : trim(str_replace('\\', '', $event->properties['LOCATION'][0]->value)); $eventrecord->uuid = $event->properties['UID'][0]->value; $eventrecord->timemodified = time(); // Add the iCal subscription details if required. // We should never do anything with an event without a subscription reference. $sub = calendar_get_subscription($subscriptionid); $eventrecord->subscriptionid = $subscriptionid; $eventrecord->userid = $sub->userid; $eventrecord->groupid = $sub->groupid; $eventrecord->courseid = $sub->courseid; $eventrecord->categoryid = $sub->categoryid; $eventrecord->eventtype = $sub->eventtype; if ($updaterecord = $DB->get_record('event', array('uuid' => $eventrecord->uuid, 'subscriptionid' => $eventrecord->subscriptionid))) { // Compare iCal event data against the moodle event to see if something has changed. $result = array_diff((array) $eventrecord, (array) $updaterecord); // Unset timemodified field because it's always going to be different. unset($result['timemodified']); if (count($result)) { $eventrecord->id = $updaterecord->id; $return = CALENDAR_IMPORT_EVENT_UPDATED; // Update. } else { return CALENDAR_IMPORT_EVENT_SKIPPED; } } else { $return = CALENDAR_IMPORT_EVENT_INSERTED; // Insert. } if ($createdevent = \calendar_event::create($eventrecord, false)) { if (!empty($event->properties['RRULE'])) { // Repeating events. date_default_timezone_set($tz); // Change time zone to parse all events. $rrule = new \core_calendar\rrule_manager($event->properties['RRULE'][0]->value); $rrule->parse_rrule(); $rrule->create_events($createdevent); \core_date::set_default_server_timezone(); // Change time zone back to what it was. } return $return; } else { return 0; } } /** * Delete subscription and all related events. * * @param int|stdClass $subscription subscription or it's id, which needs to be deleted. */ function calendar_delete_subscription($subscription) { global $DB; if (!is_object($subscription)) { $subscription = $DB->get_record('event_subscriptions', array('id' => $subscription), '*', MUST_EXIST); } // Delete subscription and related events. $DB->delete_records('event', array('subscriptionid' => $subscription->id)); $DB->delete_records('event_subscriptions', array('id' => $subscription->id)); \cache_helper::invalidate_by_definition('core', 'calendar_subscriptions', array(), array($subscription->id)); // Trigger event, calendar subscription deleted. $eventparams = array('objectid' => $subscription->id, 'context' => calendar_get_calendar_context($subscription), 'other' => array( 'eventtype' => $subscription->eventtype, ) ); switch ($subscription->eventtype) { case 'category': $eventparams['other']['categoryid'] = $subscription->categoryid; break; case 'course': $eventparams['other']['courseid'] = $subscription->courseid; break; case 'group': $eventparams['other']['courseid'] = $subscription->courseid; $eventparams['other']['groupid'] = $subscription->groupid; break; default: $eventparams['other']['courseid'] = $subscription->courseid; } $event = \core\event\calendar_subscription_deleted::create($eventparams); $event->trigger(); } /** * From a URL, fetch the calendar and return an iCalendar object. * * @param string $url The iCalendar URL * @return iCalendar The iCalendar object */ function calendar_get_icalendar($url) { global $CFG; require_once($CFG->libdir . '/filelib.php'); require_once($CFG->libdir . '/bennu/bennu.inc.php'); $curl = new \curl(); $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => 1, 'CURLOPT_MAXREDIRS' => 5)); $calendar = $curl->get($url); // Http code validation should actually be the job of curl class. if (!$calendar || $curl->info['http_code'] != 200 || !empty($curl->errorno)) { throw new \moodle_exception('errorinvalidicalurl', 'calendar'); } $ical = new \iCalendar(); $ical->unserialize($calendar); return $ical; } /** * Import events from an iCalendar object into a course calendar. * * @param iCalendar $ical The iCalendar object. * @param int|null $subscriptionid The subscription ID. * @return array A log of the import progress, including errors. */ function calendar_import_events_from_ical(iCalendar $ical, ?int $subscriptionid = null): array { global $DB; $errors = []; $eventcount = 0; $updatecount = 0; $skippedcount = 0; $deletedcount = 0; // Large calendars take a while... if (!CLI_SCRIPT) { \core_php_time_limit::raise(300); } // Start with a safe default timezone. $timezone = 'UTC'; // Grab the timezone from the iCalendar file to be used later. if (isset($ical->properties['X-WR-TIMEZONE'][0]->value)) { $timezone = $ical->properties['X-WR-TIMEZONE'][0]->value; } else if (isset($ical->properties['PRODID'][0]->value)) { // If the timezone was not found, check to se if this is MS exchange / Office 365 which uses Windows timezones. if (strncmp($ical->properties['PRODID'][0]->value, 'Microsoft', 9) == 0) { if (isset($ical->components['VTIMEZONE'][0]->properties['TZID'][0]->value)) { $tzname = $ical->components['VTIMEZONE'][0]->properties['TZID'][0]->value; $timezone = IntlTimeZone::getIDForWindowsID($tzname); } } } $icaluuids = []; foreach ($ical->components['VEVENT'] as $event) { $icaluuids[] = $event->properties['UID'][0]->value; $res = calendar_add_icalendar_event($event, null, $subscriptionid, $timezone); switch ($res) { case CALENDAR_IMPORT_EVENT_UPDATED: $updatecount++; break; case CALENDAR_IMPORT_EVENT_INSERTED: $eventcount++; break; case CALENDAR_IMPORT_EVENT_SKIPPED: $skippedcount++; break; case 0: if (empty($event->properties['SUMMARY'])) { $errors[] = '(' . get_string('notitle', 'calendar') . ')'; } else { $errors[] = $event->properties['SUMMARY'][0]->value; } break; } } $existing = $DB->get_field('event_subscriptions', 'lastupdated', ['id' => $subscriptionid]); if (!empty($existing)) { $eventsuuids = $DB->get_records_menu('event', ['subscriptionid' => $subscriptionid], '', 'id, uuid'); $icaleventscount = count($icaluuids); $tobedeleted = []; if (count($eventsuuids) > $icaleventscount) { foreach ($eventsuuids as $eventid => $eventuuid) { if (!in_array($eventuuid, $icaluuids)) { $tobedeleted[] = $eventid; } } if (!empty($tobedeleted)) { $DB->delete_records_list('event', 'id', $tobedeleted); $deletedcount = count($tobedeleted); } } } $result = [ 'eventsimported' => $eventcount, 'eventsskipped' => $skippedcount, 'eventsupdated' => $updatecount, 'eventsdeleted' => $deletedcount, 'haserror' => !empty($errors), 'errors' => $errors, ]; return $result; } /** * Fetch a calendar subscription and update the events in the calendar. * * @param int $subscriptionid The course ID for the calendar. * @return array A log of the import progress, including errors. */ function calendar_update_subscription_events($subscriptionid) { $sub = calendar_get_subscription($subscriptionid); // Don't update a file subscription. if (empty($sub->url)) { return 'File subscription not updated.'; } $ical = calendar_get_icalendar($sub->url); $return = calendar_import_events_from_ical($ical, $subscriptionid); $sub->lastupdated = time(); calendar_update_subscription($sub); return $return; } /** * Update a calendar subscription. Also updates the associated cache. * * @param stdClass|array $subscription Subscription record. * @throws coding_exception If something goes wrong * @since Moodle 2.5 */ function calendar_update_subscription($subscription) { global $DB; if (is_array($subscription)) { $subscription = (object)$subscription; } if (empty($subscription->id) || !$DB->record_exists('event_subscriptions', array('id' => $subscription->id))) { throw new \coding_exception('Cannot update a subscription without a valid id'); } $DB->update_record('event_subscriptions', $subscription); // Update cache. $cache = \cache::make('core', 'calendar_subscriptions'); $cache->set($subscription->id, $subscription); // Trigger event, calendar subscription updated. $eventparams = array('userid' => $subscription->userid, 'objectid' => $subscription->id, 'context' => calendar_get_calendar_context($subscription), 'other' => array( 'eventtype' => $subscription->eventtype, ) ); switch ($subscription->eventtype) { case 'category': $eventparams['other']['categoryid'] = $subscription->categoryid; break; case 'course': $eventparams['other']['courseid'] = $subscription->courseid; break; case 'group': $eventparams['other']['courseid'] = $subscription->courseid; $eventparams['other']['groupid'] = $subscription->groupid; break; default: $eventparams['other']['courseid'] = $subscription->courseid; } $event = \core\event\calendar_subscription_updated::create($eventparams); $event->trigger(); } /** * Checks to see if the user can edit a given subscription feed. * * @param mixed $subscriptionorid Subscription object or id * @return bool true if current user can edit the subscription else false */ function calendar_can_edit_subscription($subscriptionorid) { global $USER; if (is_array($subscriptionorid)) { $subscription = (object)$subscriptionorid; } else if (is_object($subscriptionorid)) { $subscription = $subscriptionorid; } else { $subscription = calendar_get_subscription($subscriptionorid); } $allowed = new \stdClass; $courseid = $subscription->courseid; $categoryid = $subscription->categoryid; $groupid = $subscription->groupid; $category = null; if (!empty($categoryid)) { $category = \core_course_category::get($categoryid); } calendar_get_allowed_types($allowed, $courseid, null, $category); switch ($subscription->eventtype) { case 'user': return ($USER->id == $subscription->userid && $allowed->user); case 'course': if (isset($allowed->courses[$courseid])) { return $allowed->courses[$courseid]; } else { return false; } case 'category': if (isset($allowed->categories[$categoryid])) { return $allowed->categories[$categoryid]; } else { return false; } case 'site': return $allowed->site; case 'group': if (isset($allowed->groups[$groupid])) { return $allowed->groups[$groupid]; } else { return false; } default: return false; } } /** * Helper function to determine the context of a calendar subscription. * Subscriptions can be created in two contexts COURSE, or USER. * * @param stdClass $subscription * @return context instance */ function calendar_get_calendar_context($subscription) { // Determine context based on calendar type. if ($subscription->eventtype === 'site') { $context = \context_course::instance(SITEID); } else if ($subscription->eventtype === 'group' || $subscription->eventtype === 'course') { $context = \context_course::instance($subscription->courseid); } else { $context = \context_user::instance($subscription->userid); } return $context; } /** * Implements callback user_preferences, lists preferences that users are allowed to update directly * * Used in {@see core_user::fill_preferences_cache()}, see also {@see useredit_update_user_preference()} * * @return array */ function core_calendar_user_preferences() { $preferences = []; $preferences['calendar_timeformat'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED, 'default' => '0', 'choices' => array('0', CALENDAR_TF_12, CALENDAR_TF_24) ); $preferences['calendar_startwday'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0, 'choices' => array(0, 1, 2, 3, 4, 5, 6)); $preferences['calendar_maxevents'] = array('type' => PARAM_INT, 'choices' => range(1, 20)); $preferences['calendar_lookahead'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 365, 'choices' => array(365, 270, 180, 150, 120, 90, 60, 30, 21, 14, 7, 6, 5, 4, 3, 2, 1)); $preferences['calendar_persistflt'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0, 'choices' => array(0, 1)); return $preferences; } /** * Get legacy calendar events * * @param int $tstart Start time of time range for events * @param int $tend End time of time range for events * @param array|int|boolean $users array of users, user id or boolean for all/no user events * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events * @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 $categories array of category ids and/or objects. * @param int $limitnum Number of events to fetch or zero to fetch all. * * @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, $categories = [], $limitnum = 0) { // 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, $categoryparam) = array_map(function($param) { // If parameter is true, return null. if ($param === true) { return null; } // If parameter is false, return an empty array. if ($param === false) { return []; } // If the parameter is a scalar value, enclose it in an array. if (!is_array($param)) { return [$param]; } // No normalisation required. return $param; }, [$users, $groups, $courses, $categories]); // If a single user is provided, we can use that for capability checks. // Otherwise current logged in user is used - See MDL-58768. if (is_array($userparam) && count($userparam) == 1) { \core_calendar\local\event\container::set_requesting_user($userparam[0]); } $mapper = \core_calendar\local\event\container::get_event_mapper(); $events = \core_calendar\local\api::get_events( $tstart, $tend, null, null, null, null, $limitnum, null, $userparam, $groupparam, $courseparam, $categoryparam, $withduration, $ignorehidden ); return array_reduce($events, function($carry, $event) use ($mapper) { return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)]; }, []); } /** * Get the calendar view output. * * @param \calendar_information $calendar The calendar being represented * @param string $view The type of calendar to have displayed * @param bool $includenavigation Whether to include navigation * @param bool $skipevents Whether to load the events or not * @param int $lookahead Overwrites site and users's lookahead setting. * @return array[array, string] */ function calendar_get_view(\calendar_information $calendar, $view, $includenavigation = true, bool $skipevents = false, ?int $lookahead = null) { global $PAGE, $CFG; $renderer = $PAGE->get_renderer('core_calendar'); $type = \core_calendar\type_factory::get_calendar_instance(); // Calculate the bounds of the month. $calendardate = $type->timestamp_to_date_array($calendar->time); $date = new \DateTime('now', core_date::get_user_timezone_object(99)); $eventlimit = 0; if ($view === 'day') { $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], $calendardate['mday']); $date->setTimestamp($tstart); $date->modify('+1 day'); } else if ($view === 'upcoming' || $view === 'upcoming_mini') { // Number of days in the future that will be used to fetch events. if (!$lookahead) { if (isset($CFG->calendar_lookahead)) { $defaultlookahead = intval($CFG->calendar_lookahead); } else { $defaultlookahead = CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD; } $lookahead = get_user_preferences('calendar_lookahead', $defaultlookahead); } // Maximum number of events to be displayed on upcoming view. $defaultmaxevents = CALENDAR_DEFAULT_UPCOMING_MAXEVENTS; if (isset($CFG->calendar_maxevents)) { $defaultmaxevents = intval($CFG->calendar_maxevents); } $eventlimit = get_user_preferences('calendar_maxevents', $defaultmaxevents); $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], $calendardate['mday'], $calendardate['hours']); $date->setTimestamp($tstart); $date->modify('+' . $lookahead . ' days'); } else { $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], 1); $monthdays = $type->get_num_days_in_month($calendardate['year'], $calendardate['mon']); $date->setTimestamp($tstart); $date->modify('+' . $monthdays . ' days'); if ($view === 'mini' || $view === 'minithree') { $template = 'core_calendar/calendar_mini'; } else { $template = 'core_calendar/calendar_month'; } } // We need to extract 1 second to ensure that we don't get into the next day. $date->modify('-1 second'); $tend = $date->getTimestamp(); list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) { // If parameter is true, return null. if ($param === true) { return null; } // If parameter is false, return an empty array. if ($param === false) { return []; } // If the parameter is a scalar value, enclose it in an array. if (!is_array($param)) { return [$param]; } // No normalisation required. return $param; }, [$calendar->users, $calendar->groups, $calendar->courses, $calendar->categories]); if ($skipevents) { $events = []; } else { $events = \core_calendar\local\api::get_events( $tstart, $tend, null, null, null, null, $eventlimit, null, $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; } ); } $related = [ 'events' => $events, 'cache' => new \core_calendar\external\events_related_objects_cache($events), 'type' => $type, ]; $data = []; $calendar->set_viewmode($view); if ($view == "month" || $view == "monthblock" || $view == "mini" || $view == "minithree" ) { $month = new \core_calendar\external\month_exporter($calendar, $type, $related); $month->set_includenavigation($includenavigation); $month->set_initialeventsloaded(!$skipevents); $month->set_showcoursefilter(($view == "month" || $view == "monthblock")); $data = $month->export($renderer); } else if ($view == "day") { $day = new \core_calendar\external\calendar_day_exporter($calendar, $related); $data = $day->export($renderer); $data->viewingday = true; $data->showviewselector = true; $template = 'core_calendar/calendar_day'; } else if ($view == "upcoming" || $view == "upcoming_mini") { $upcoming = new \core_calendar\external\calendar_upcoming_exporter($calendar, $related); $data = $upcoming->export($renderer); if ($view == "upcoming") { $template = 'core_calendar/calendar_upcoming'; $data->viewingupcoming = true; $data->showviewselector = true; } else if ($view == "upcoming_mini") { $template = 'core_calendar/calendar_upcoming_mini'; } } // Check if $data has events. if (isset($data->events)) { // Let's check and sanitize all "name" in $data->events before it's sent to front end. foreach ($data->events as $d) { $name = $d->name ?? null; // Encode special characters if our decoded name does not match the original name. if ($name && (html_entity_decode($name) !== $name)) { $d->name = htmlspecialchars(html_entity_decode($name), ENT_QUOTES, 'utf-8'); } } } return [$data, $template]; } /** * Request and render event form fragment. * * @param array $args The fragment arguments. * @return string The rendered mform fragment. */ function calendar_output_fragment_event_form($args) { global $CFG, $OUTPUT, $USER; require_once($CFG->libdir . '/grouplib.php'); $html = ''; $data = []; $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']) && $args['courseid'] != SITEID) ? 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); $editoroptions = \core_calendar\local\event\forms\create::build_editor_options($context); $formoptions = ['editoroptions' => $editoroptions, 'courseid' => $courseid]; $draftitemid = 0; if ($hasformdata) { parse_str(clean_param($args['formdata'], PARAM_TEXT), $data); if (isset($data['description']['itemid'])) { $draftitemid = $data['description']['itemid']; } } if ($starttime) { $formoptions['starttime'] = $starttime; } // Let's check first which event types user can add. $eventtypes = calendar_get_allowed_event_types($courseid); $formoptions['eventtypes'] = $eventtypes; if (is_null($eventid)) { if (!empty($courseid)) { $groupcoursedata = groups_get_all_groups($courseid); $formoptions['groups'] = []; foreach ($groupcoursedata as $groupid => $groupdata) { $formoptions['groups'][$groupid] = format_string($groupdata->name, true, ['context' => $context]); } } $mform = new \core_calendar\local\event\forms\create( null, $formoptions, 'post', '', null, true, $data ); // If the user is on course context and is allowed to add course events set the event type default to course. if (!empty($courseid) && !empty($eventtypes['course'])) { $data['eventtype'] = 'course'; $data['courseid'] = $courseid; $data['groupcourseid'] = $courseid; } else if (!empty($categoryid) && !empty($eventtypes['category'])) { $data['eventtype'] = 'category'; $data['categoryid'] = $categoryid; } else if (!empty($groupcoursedata) && !empty($eventtypes['group'])) { $data['groupcourseid'] = $courseid; $data['groups'] = $groupcoursedata->groups; } $mform->set_data($data); } else { $event = calendar_event::load($eventid); if (!calendar_edit_event_allowed($event)) { throw new \moodle_exception('nopermissiontoupdatecalendar'); } $mapper = new \core_calendar\local\event\mappers\create_update_form_mapper(); $eventdata = $mapper->from_legacy_event_to_data($event); $data = array_merge((array) $eventdata, $data); $event->count_repeats(); $formoptions['event'] = $event; if (!empty($event->courseid)) { $groupcoursedata = groups_get_all_groups($event->courseid); $formoptions['groups'] = []; foreach ($groupcoursedata as $groupid => $groupdata) { $formoptions['groups'][$groupid] = format_string($groupdata->name, true, ['context' => $context]); } } $data['description']['text'] = file_prepare_draft_area( $draftitemid, $event->context->id, 'calendar', 'event_description', $event->id, null, $data['description']['text'] ); $data['description']['itemid'] = $draftitemid; $mform = new \core_calendar\local\event\forms\update( null, $formoptions, 'post', '', null, true, $data ); $mform->set_data($data); // Check to see if this event is part of a subscription or import. // If so display a warning on edit. if (isset($event->subscriptionid) && ($event->subscriptionid != null)) { $renderable = new \core\output\notification( get_string('eventsubscriptioneditwarning', 'calendar'), \core\output\notification::NOTIFY_INFO ); $html .= $OUTPUT->render($renderable); } } if ($hasformdata) { $mform->is_validated(); } $html .= $mform->render(); return $html; } /** * Calculate the timestamp from the supplied Gregorian Year, Month, and Day. * * @param int $d The day * @param int $m The month * @param int $y The year * @param int $time The timestamp to use instead of a separate y/m/d. * @return int The timestamp */ function calendar_get_timestamp($d, $m, $y, $time = 0) { // 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. if (!empty($d) && !empty($m) && !empty($y)) { if (checkdate($m, $d, $y)) { $time = make_timestamp($y, $m, $d); } else { $time = time(); } } else if (empty($time)) { $time = time(); } return $time; } /** * Get the calendar footer options. * * @param calendar_information $calendar The calendar information object. * @param array $options Display options for the footer. If an option is not set, a default value will be provided. * It consists of: * - showfullcalendarlink - bool - Whether to show the full calendar link or not. Defaults to false. * * @return array The data for template and template name. */ function calendar_get_footer_options($calendar, array $options = []) { global $CFG, $USER, $PAGE; // Generate hash for iCal link. $authtoken = calendar_get_export_token($USER); $renderer = $PAGE->get_renderer('core_calendar'); $footer = new \core_calendar\external\footer_options_exporter($calendar, $USER->id, $authtoken, $options); $data = $footer->export($renderer); $template = 'core_calendar/footer_options'; return [$data, $template]; } /** * Get the list of potential calendar filter types as a type => name * combination. * * @return array */ function calendar_get_filter_types() { $types = [ 'site', 'category', 'course', 'group', 'user', 'other' ]; return array_map(function($type) { return [ 'eventtype' => $type, 'name' => get_string("eventtype{$type}", "calendar"), 'icon' => true, 'key' => 'i/' . $type . 'event', 'component' => 'core' ]; }, $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); } /** * Get event types the user can create event based on categories, courses and groups * the logged in user belongs to. * * @param int|null $courseid The course id. * @return array The array of allowed types. */ function calendar_get_allowed_event_types(?int $courseid = null) { global $DB, $CFG, $USER; $types = [ 'user' => false, 'site' => false, 'course' => false, 'group' => false, 'category' => false ]; if (!empty($courseid) && $courseid != SITEID) { $context = \context_course::instance($courseid); $types['user'] = has_capability('moodle/calendar:manageownentries', $context); calendar_internal_update_course_and_group_permission($courseid, $context, $types); } if (has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID))) { $types['site'] = true; } if (has_capability('moodle/calendar:manageownentries', \context_system::instance())) { $types['user'] = true; } if (core_course_category::has_manage_capability_on_any()) { $types['category'] = true; } // We still don't know if the user can create group and course events, so iterate over the courses to find out // if the user has capabilities in one of the courses. if ($types['course'] == false || $types['group'] == false) { if ($CFG->calendar_adminseesall && has_capability('moodle/calendar:manageentries', context_system::instance())) { $sql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . " FROM {course} c JOIN {context} ctx ON ctx.contextlevel = ? AND ctx.instanceid = c.id WHERE c.id IN ( SELECT DISTINCT courseid FROM {groups} )"; $courseswithgroups = $DB->get_recordset_sql($sql, [CONTEXT_COURSE]); foreach ($courseswithgroups as $course) { context_helper::preload_from_record($course); $context = context_course::instance($course->id); if (has_capability('moodle/calendar:manageentries', $context)) { if (has_any_capability(['moodle/site:accessallgroups', 'moodle/calendar:managegroupentries'], $context)) { // The user can manage group entries or access any group. $types['group'] = true; $types['course'] = true; break; } } } $courseswithgroups->close(); if (false === $types['course']) { // Course is still not confirmed. There may have been no courses with a group in them. $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); $sql = "SELECT c.id, c.visible, {$ctxfields} FROM {course} c JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; $params = [ 'contextlevel' => CONTEXT_COURSE, ]; $courses = $DB->get_recordset_sql($sql, $params); foreach ($courses as $course) { context_helper::preload_from_record($course); $context = context_course::instance($course->id); if (has_capability('moodle/calendar:manageentries', $context)) { $types['course'] = true; break; } } $courses->close(); } } else { $courses = calendar_get_default_courses(null, 'id'); if (empty($courses)) { return $types; } $courseids = array_map(function($c) { return $c->id; }, $courses); // Check whether the user has access to create events within courses which have groups. list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); $sql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . " FROM {course} c JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id WHERE c.id $insql AND c.id IN (SELECT DISTINCT courseid FROM {groups})"; $params['contextlevel'] = CONTEXT_COURSE; $courseswithgroups = $DB->get_recordset_sql($sql, $params); foreach ($courseswithgroups as $coursewithgroup) { context_helper::preload_from_record($coursewithgroup); $context = context_course::instance($coursewithgroup->id); calendar_internal_update_course_and_group_permission($coursewithgroup->id, $context, $types); // Okay, course and group event types are allowed, no need to keep the loop iteration. if ($types['course'] == true && $types['group'] == true) { break; } } $courseswithgroups->close(); if (false === $types['course']) { list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); $contextsql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . " FROM {course} c JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id WHERE c.id $insql"; $params['contextlevel'] = CONTEXT_COURSE; $contextrecords = $DB->get_recordset_sql($contextsql, $params); foreach ($contextrecords as $course) { context_helper::preload_from_record($course); $coursecontext = context_course::instance($course->id); if (has_capability('moodle/calendar:manageentries', $coursecontext) && ($courseid == $course->id || empty($courseid))) { $types['course'] = true; break; } } $contextrecords->close(); } } } return $types; } /** * Given a course id, and context, updates the permission types array to add the 'course' or 'group' * permission if it is relevant for that course. * * For efficiency, if they already have 'course' or 'group' then it skips checks. * * Do not call this function directly, it is only for use by calendar_get_allowed_event_types(). * * @param int $courseid Course id * @param context $context Context for that course * @param array $types Current permissions */ function calendar_internal_update_course_and_group_permission(int $courseid, context $context, array &$types) { if (!$types['course']) { // If they have manageentries permission on the course, then they can update this course. if (has_capability('moodle/calendar:manageentries', $context)) { $types['course'] = true; } } // To update group events they must have EITHER manageentries OR managegroupentries. if (!$types['group'] && (has_capability('moodle/calendar:manageentries', $context) || has_capability('moodle/calendar:managegroupentries', $context))) { // And they also need for a group to exist on the course. $groups = groups_get_all_groups($courseid); if (!empty($groups)) { // And either accessallgroups, or belong to one of the groups. if (has_capability('moodle/site:accessallgroups', $context)) { $types['group'] = true; } else { foreach ($groups as $group) { if (groups_is_member($group->id)) { $types['group'] = true; break; } } } } } } /** * Get the auth token for exporting the given user calendar. * @param stdClass $user The user to export the calendar for * * @return string The export token. */ function calendar_get_export_token(stdClass $user): string { global $CFG, $DB; return sha1($user->id . $DB->get_field('user', 'password', ['id' => $user->id]) . $CFG->calendar_exportsalt); } /** * Get the list of URL parameters for calendar expport and import links. * * @return array */ function calendar_get_export_import_link_params(): array { global $PAGE; $params = []; if ($courseid = $PAGE->url->get_param('course')) { $params['course'] = $courseid; } if ($categoryid = $PAGE->url->get_param('category')) { $params['category'] = $categoryid; } return $params; } /** * Implements the inplace editable feature. * * @param string $itemtype Type of the inplace editable element * @param int $itemid Id of the item to edit * @param int $newvalue New value of the item * @return \core\output\inplace_editable */ function calendar_inplace_editable(string $itemtype, int $itemid, int $newvalue): \core\output\inplace_editable { global $OUTPUT; if ($itemtype === 'refreshinterval') { $subscription = calendar_get_subscription($itemid); $context = calendar_get_calendar_context($subscription); external_api::validate_context($context); $updateresult = \core_calendar\output\refreshintervalcollection::update($itemid, $newvalue); $refreshresults = calendar_update_subscription_events($itemid); \core\notification::add($OUTPUT->render_from_template( 'core_calendar/subscription_update_result', array_merge($refreshresults, [ 'subscriptionname' => s($subscription->name), ]) ), \core\notification::INFO); return $updateresult; } external_api::validate_context(context_system::instance()); } PK*\_"" export.phpnu[dirroot.'/course/lib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); if (empty($CFG->enablecalendarexport)) { die('no export'); } $courseid = optional_param('course', SITEID, PARAM_INT); $categoryid = optional_param('category', null, PARAM_INT); $action = optional_param('action', '', PARAM_ALPHA); $day = optional_param('cal_d', 0, PARAM_INT); $mon = optional_param('cal_m', 0, PARAM_INT); $year = optional_param('cal_y', 0, PARAM_INT); $time = optional_param('time', 0, PARAM_INT); $generateurl = optional_param('generateurl', 0, PARAM_BOOL); // 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($day) && !empty($mon) && !empty($year)) { if (checkdate($mon, $day, $year)) { $time = make_timestamp($year, $mon, $day); } else { $time = time(); } } else if (empty($time)) { $time = time(); } $url = new moodle_url('/calendar/export.php', array('time' => $time)); $managesubscriptionsurl = new moodle_url('/calendar/managesubscriptions.php'); if ($courseid != SITEID && !empty($courseid)) { // Course ID must be valid and existing. $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); $courses = array($course->id => $course); $url->param('course', $course->id); $managesubscriptionsurl->param('course', $course->id); navigation_node::override_active_url(new moodle_url('/course/view.php', ['id' => $course->id])); $PAGE->navbar->add( get_string('calendar', 'calendar'), new moodle_url('/calendar/view.php', ['view' => 'month', 'course' => $course->id]) ); } else { $course = get_site(); $courses = calendar_get_default_courses(); if (!empty($categoryid)) { $managesubscriptionsurl->param('category', $categoryid); navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $categoryid])); $PAGE->set_category_by_id($categoryid); $PAGE->navbar->add( get_string('calendar', 'calendar'), new moodle_url('/calendar/view.php', ['view' => 'month', 'category' => $categoryid]) ); } else { $PAGE->navbar->add(get_string('calendar', 'calendar'), new moodle_url('/calendar/view.php', ['view' => 'month'])); } } require_login($course, false); if ($action !== '') { $url->param('action', $action); } $PAGE->set_url($url); $calendar = new calendar_information(0, 0, 0, $time); $calendar->set_sources($course, $courses); $pagetitle = get_string('export', 'calendar'); $PAGE->navbar->add(get_string('managesubscriptions', 'calendar'), $managesubscriptionsurl); $PAGE->navbar->add(get_string('exportcalendar', 'calendar'), $url); // Print title and header. $headingstr = get_string('calendar', 'core_calendar'); $headingstr = ($courseid != SITEID && !empty($courseid)) ? "{$headingstr}: {$COURSE->shortname}" : $headingstr; $PAGE->set_title($course->shortname.': '.get_string('calendar', 'calendar').': '.$pagetitle); $PAGE->set_heading($headingstr); $PAGE->set_pagelayout('standard'); $PAGE->set_secondary_navigation(false); $renderer = $PAGE->get_renderer('core_calendar'); $calendar->add_sidecalendar_blocks($renderer); // Get the calendar type we are using. $calendartype = \core_calendar\type_factory::get_calendar_instance(); $now = $calendartype->timestamp_to_date_array($time); $weekend = CALENDAR_DEFAULT_WEEKEND; if (isset($CFG->calendar_weekend)) { $weekend = intval($CFG->calendar_weekend); } $numberofdaysinweek = $calendartype->get_num_weekdays(); $formdata = array( // Let's populate some vars to let "common tasks" be somewhat smart... // If today it's weekend, give the "next week" option. 'allownextweek' => $weekend & (1 << $now['wday']), // If it's the last week of the month, give the "next month" option. 'allownextmonth' => calendar_days_in_month($now['mon'], $now['year']) - $now['mday'] < $numberofdaysinweek, // If today it's weekend but tomorrow it isn't, do NOT give the "this week" option. 'allowthisweek' => !(($weekend & (1 << $now['wday'])) && !($weekend & (1 << (($now['wday'] + 1) % $numberofdaysinweek)))) ); // Disable submit protection so that the submit buttons continue working after being pressed. $exportform = new core_calendar_export_form($FULLME, $formdata, 'POST', '', ['data-double-submit-protection' => 'off']); $calendarurl = ''; if ($data = $exportform->get_data()) { $params = array(); $params['userid'] = $USER->id; $params['authtoken'] = calendar_get_export_token($USER); $params['preset_what'] = $data->events['exportevents']; $params['preset_time'] = $data->period['timeperiod']; $link = new moodle_url('/calendar/export_execute.php', $params); if (!empty($data->generateurl)) { $exporturlcontext = ['calendarexporturl' => $link->out(false)]; $exporturl = $OUTPUT->render_from_template('core_calendar/export_calendar_url', $exporturlcontext); $calendarurl = html_writer::div($exporturl, 'generalbox calendarurl mt-3'); } if (!empty($data->export)) { redirect($link); } } echo $OUTPUT->header(); echo $renderer->start_layout(); echo $OUTPUT->heading(get_string('exportcalendar', 'calendar')); if ($action != 'advanced') { $exportform->display(); } echo $calendarurl; echo $renderer->complete_layout(); echo $OUTPUT->footer(); PK*\ƑH H event.phpnu[dirroot.'/calendar/event_form.php'); require_once($CFG->dirroot.'/calendar/lib.php'); require_once($CFG->dirroot.'/course/lib.php'); require_login(); $action = optional_param('action', 'new', PARAM_ALPHA); $eventid = optional_param('id', 0, PARAM_INT); $courseid = optional_param('courseid', SITEID, PARAM_INT); $courseid = optional_param('course', $courseid, PARAM_INT); $day = optional_param('cal_d', 0, PARAM_INT); $month = optional_param('cal_m', 0, PARAM_INT); $year = optional_param('cal_y', 0, PARAM_INT); $time = optional_param('time', 0, PARAM_INT); // 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($day) && !empty($month) && !empty($year)) { if (checkdate($month, $day, $year)) { $time = make_timestamp($year, $month, $day); } else { $time = time(); } } else if (empty($time)) { $time = time(); } $url = new moodle_url('/calendar/event.php', array('action' => $action)); if ($eventid != 0) { $url->param('id', $eventid); } if ($courseid != SITEID) { $url->param('course', $courseid); } $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); if ($courseid != SITEID && !empty($courseid)) { $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); $courses = array($course->id => $course); $issite = false; } else { $course = get_site(); $courses = calendar_get_default_courses(); $issite = true; } require_login($course, false); if ($action === 'delete' && $eventid > 0) { $deleteurl = new moodle_url('/calendar/delete.php', array('id'=>$eventid)); if ($courseid > 0) { $deleteurl->param('course', $courseid); } redirect($deleteurl); } $calendar = new calendar_information(0, 0, 0, $time); $calendar->set_sources($course, $courses); $formoptions = new stdClass; if ($eventid !== 0) { $title = get_string('editevent', 'calendar'); $event = calendar_event::load($eventid); if (!calendar_edit_event_allowed($event, true)) { throw new \moodle_exception('nopermissions'); } $event->action = $action; $event->course = $courseid; $event->timedurationuntil = $event->timestart + $event->timeduration; $event->count_repeats(); if (!calendar_add_event_allowed($event)) { throw new \moodle_exception('nopermissions'); } // Check to see if this event is part of a subscription or import. // If so display a warning on edit. if (isset($event->subscriptionid) && ($event->subscriptionid != null)) { \core\notification::add(get_string('eventsubscriptioneditwarning', 'calendar'), \core\output\notification::NOTIFY_INFO); } } else { $title = get_string('newevent', 'calendar'); calendar_get_allowed_types($formoptions->eventtypes, $course); $event = new stdClass(); $event->action = $action; $event->course = $courseid; $event->courseid = $courseid; $event->timeduration = 0; if ($formoptions->eventtypes->courses) { if (!$issite) { $event->eventtype = 'course'; } else { unset($formoptions->eventtypes->courses); unset($formoptions->eventtypes->groups); } } $event->timestart = $time; $event = new calendar_event($event); if (!calendar_add_event_allowed($event)) { throw new \moodle_exception('nopermissions'); } } $properties = $event->properties(true); $formoptions->event = $event; $formoptions->hasduration = ($event->timeduration > 0); $mform = new event_form(null, $formoptions); $mform->set_data($properties); $data = $mform->get_data(); if ($data) { if ($data->duration == 1) { $data->timeduration = $data->timedurationuntil- $data->timestart; } else if ($data->duration == 2) { $data->timeduration = $data->timedurationminutes * MINSECS; } else { $data->timeduration = 0; } $event->update($data); $params = array( 'view' => 'day', 'time' => $event->timestart, ); $eventurl = new moodle_url('/calendar/view.php', $params); if (!empty($event->courseid) && $event->courseid != SITEID) { $eventurl->param('course', $event->courseid); } $eventurl->set_anchor('event_'.$event->id); redirect($eventurl); } $viewcalendarurl = new moodle_url(CALENDAR_URL.'view.php', $PAGE->url->params()); $viewcalendarurl->remove_params(array('id', 'action')); $viewcalendarurl->param('view', 'upcoming'); $strcalendar = get_string('calendar', 'calendar'); $PAGE->navbar->add($strcalendar, $viewcalendarurl); $PAGE->navbar->add($title); $PAGE->set_title($course->shortname.': '.$strcalendar.': '.$title); $PAGE->set_heading($course->fullname); $PAGE->set_secondary_navigation(false); $renderer = $PAGE->get_renderer('core_calendar'); $calendar->add_sidecalendar_blocks($renderer); echo $OUTPUT->header(); echo $OUTPUT->heading($title); $mform->display(); echo $OUTPUT->footer(); PK*\M= = 0classes/external/event_subscription_exporter.phpnu[. /** * Contains event class for displaying a calendar event's subscription. * * @package core_calendar * @copyright 2017 Simey Lameze * @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 \core_calendar\local\event\entities\event_interface; /** * Class for displaying a calendar event's subscription. * * @package core_calendar * @copyright 2017 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_subscription_exporter extends exporter { /** * Constructor. * * @param event_interface $event */ public function __construct(event_interface $event) { global $CFG; $data = new \stdClass(); $data->displayeventsource = false; if ($event->get_subscription()) { $subscription = calendar_get_subscription($event->get_subscription()->get('id')); if (!empty($subscription) && $CFG->calendar_showicalsource) { $data->displayeventsource = true; if (!empty($subscription->url)) { $data->subscriptionurl = $subscription->url; } $data->subscriptionname = $subscription->name; } } parent::__construct($data); } /** * Return the list of properties. * * @return array */ protected static function define_properties() { return [ 'displayeventsource' => [ 'type' => PARAM_BOOL ], 'subscriptionname' => [ 'type' => PARAM_RAW, 'optional' => true ], 'subscriptionurl' => [ 'type' => PARAM_URL, 'optional' => true ], ]; } } PK*\!!!classes/external/day_exporter.phpnu[. namespace core_calendar\external; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/calendar/lib.php'); use core\external\exporter; use core_date; use DateTimeImmutable; use renderer_base; use moodle_url; /** * Class for displaying the day view. * * @package core_calendar * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class day_exporter extends exporter { /** * @var \calendar_information $calendar The calendar being displayed. */ protected $calendar; /** * @var moodle_url */ protected $url; /** * Constructor. * * @param \calendar_information $calendar The calendar information for the period being displayed * @param mixed $data Either an stdClass or an array of values. * @param array $related Related objects. */ public function __construct(\calendar_information $calendar, $data, $related) { $this->calendar = $calendar; $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); } /** * Return the list of properties. * * @return array */ protected static function define_properties() { // These are the default properties as returned by getuserdate() // but without the formatted month and week names. return [ 'seconds' => [ 'type' => PARAM_INT, ], 'minutes' => [ 'type' => PARAM_INT, ], 'hours' => [ 'type' => PARAM_INT, ], 'mday' => [ 'type' => PARAM_INT, ], 'wday' => [ 'type' => PARAM_INT, ], 'year' => [ 'type' => PARAM_INT, ], 'yday' => [ 'type' => PARAM_INT, ], ]; } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'timestamp' => [ 'type' => PARAM_INT, ], 'neweventtimestamp' => [ 'type' => PARAM_INT, ], 'viewdaylink' => [ 'type' => PARAM_URL, 'optional' => true, ], 'viewdaylinktitle' => [ 'type' => PARAM_RAW, 'optional' => true, ], 'events' => [ 'type' => calendar_event_exporter::read_properties_definition(), 'multiple' => true, ], 'hasevents' => [ 'type' => PARAM_BOOL, 'default' => false, ], 'calendareventtypes' => [ 'type' => PARAM_RAW, 'multiple' => true, ], 'previousperiod' => [ 'type' => PARAM_INT, ], 'nextperiod' => [ 'type' => PARAM_INT, ], 'haslastdayofevent' => [ 'type' => PARAM_BOOL, 'default' => false, ], ]; } /** * 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) { $daytimestamp = $this->calendar->time; $timestamp = $this->data[0]; // Need to account for user's timezone. $usernow = usergetdate(time()); $today = new DateTimeImmutable( timezone: core_date::get_user_timezone_object(), ); // The start time should use the day's date but the current // time of the day (adjusted for user's timezone). $neweventstarttime = $today->setTimestamp($timestamp)->setTime( $usernow['hours'], $usernow['minutes'], $usernow['seconds'] ); $return = [ 'timestamp' => $timestamp, 'neweventtimestamp' => $neweventstarttime->getTimestamp(), 'previousperiod' => $this->get_previous_day_timestamp($daytimestamp), 'nextperiod' => $this->get_next_day_timestamp($daytimestamp), 'viewdaylink' => $this->url->out(false), ]; if ($viewdaylinktitle = $this->get_view_link_title()) { $return['viewdaylinktitle'] = $viewdaylinktitle; } $cache = $this->related['cache']; $eventexporters = array_map(function($event) use ($cache, $output) { $context = $cache->get_context($event); $course = $cache->get_course($event); $moduleinstance = $cache->get_module_instance($event); $exporter = new calendar_event_exporter($event, [ 'context' => $context, 'course' => $course, 'moduleinstance' => $moduleinstance, 'daylink' => $this->url, 'type' => $this->related['type'], 'today' => $this->data[0], ]); return $exporter; }, $this->related['events']); $return['events'] = array_map(function($exporter) use ($output) { return $exporter->export($output); }, $eventexporters); $return['hasevents'] = !empty($return['events']); $return['calendareventtypes'] = array_map(function($exporter) { return $exporter->get_calendar_event_type(); }, $eventexporters); $return['calendareventtypes'] = array_values(array_unique($return['calendareventtypes'])); $return['haslastdayofevent'] = false; foreach ($return['events'] as $event) { if ($event->islastday) { $return['haslastdayofevent'] = true; break; } } return $return; } /** * 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. * * @param int $daytimestamp The current day timestamp. * @return int The previous day timestamp. */ protected function get_previous_day_timestamp($daytimestamp) { return $this->related['type']->get_prev_day($daytimestamp); } /** * Get the next day timestamp. * * @param int $daytimestamp The current day timestamp. * @return int The next day timestamp. */ protected function get_next_day_timestamp($daytimestamp) { return $this->related['type']->get_next_day($daytimestamp); } /** * Get the title for view link. * * @return string */ protected function get_view_link_title() { $title = null; $userdate = userdate($this->data[0], get_string('strftimedayshort')); if ($this->data['istoday']) { $title = get_string('todayplustitle', 'calendar', $userdate); } else if (count($this->related['events'])) { $title = get_string('eventsfor', 'calendar', $userdate); } return $title; } } PK*\+)!S>>#classes/external/month_exporter.phpnu[. /** * Contains event class for displaying the month view. * * @package core_calendar * @copyright 2017 Andrew Nicols * @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; /** * Class for displaying the month view. * * @package core_calendar * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class month_exporter extends exporter { /** @var int Number of calendar instances displayed. */ protected static $calendarinstances = 0; /** @var int This calendar instance's ID. */ protected $calendarinstanceid = 0; /** * @var \calendar_information $calendar The calendar to be rendered. */ protected $calendar; /** * @var int $firstdayofweek The first day of the week. */ protected $firstdayofweek; /** * @var moodle_url $url The URL for the events page. */ protected $url; /** * @var bool $includenavigation Whether navigation should be included on the output. */ protected $includenavigation = true; /** * @var bool $initialeventsloaded Whether the events have been loaded for this month. */ protected $initialeventsloaded = true; /** * @var bool $showcoursefilter Whether to render the course filter selector as well. */ protected $showcoursefilter = false; /** * Constructor for month_exporter. * * @param \calendar_information $calendar The calendar being represented * @param \core_calendar\type_base $type The calendar type (e.g. Gregorian) * @param array $related The related information */ public function __construct(\calendar_information $calendar, \core_calendar\type_base $type, $related) { // Increment the calendar instances count on initialisation. self::$calendarinstances++; // Assign this instance an ID based on the latest calendar instances count. $this->calendarinstanceid = self::$calendarinstances; $this->calendar = $calendar; $this->firstdayofweek = $type->get_starting_weekday(); $this->url = new moodle_url('/calendar/view.php', [ 'view' => 'month', 'time' => $calendar->time, ]); 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; $data = [ 'url' => $this->url->out(false), ]; parent::__construct($data, $related); } protected static function define_properties() { return [ 'url' => [ 'type' => PARAM_URL, ], ]; } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'courseid' => [ 'type' => PARAM_INT, ], 'categoryid' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => 0, ], 'filter_selector' => [ 'type' => PARAM_RAW, 'optional' => true, ], 'weeks' => [ 'type' => week_exporter::read_properties_definition(), 'multiple' => true, ], 'daynames' => [ 'type' => day_name_exporter::read_properties_definition(), 'multiple' => true, ], 'view' => [ 'type' => PARAM_ALPHA, ], '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, ], 'includenavigation' => [ 'type' => PARAM_BOOL, 'default' => true, ], // Tracks whether the first set of events have been loaded and provided // to the exporter. 'initialeventsloaded' => [ 'type' => PARAM_BOOL, 'default' => true, ], '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, ], 'defaulteventcontext' => [ 'type' => PARAM_INT, 'default' => 0, ], 'calendarinstanceid' => [ 'type' => PARAM_INT, 'default' => 0, ], 'viewingmonth' => [ 'type' => PARAM_BOOL, 'default' => true, ], 'showviewselector' => [ 'type' => PARAM_BOOL, 'default' => true, ], 'viewinginblock' => [ 'type' => PARAM_BOOL, 'default' => false, ], ]; } /** * 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) { $previousperiod = $this->get_previous_month_data(); $nextperiod = $this->get_next_month_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]); $viewmode = $this->calendar->get_viewmode() ?? 'month'; $return = [ 'courseid' => $this->calendar->courseid, 'weeks' => $this->get_weeks($output), 'daynames' => $this->get_day_names($output), 'view' => $viewmode, 'date' => (new date_exporter($date))->export($output), 'periodname' => userdate($this->calendar->time, get_string('strftimemonthyear')), 'previousperiod' => (new date_exporter($previousperiod))->export($output), 'previousperiodname' => userdate($previousperiod[0], get_string('strftimemonth')), 'previousperiodlink' => $previousperiodlink->out(false), 'nextperiod' => (new date_exporter($nextperiod))->export($output), 'nextperiodname' => userdate($nextperiod[0], get_string('strftimemonth')), 'nextperiodlink' => $nextperiodlink->out(false), 'larrow' => $output->larrow(), 'rarrow' => $output->rarrow(), 'includenavigation' => $this->includenavigation, 'initialeventsloaded' => $this->initialeventsloaded, 'calendarinstanceid' => $this->calendarinstanceid, 'showviewselector' => $viewmode === 'month', 'viewinginblock' => $viewmode === 'monthblock', ]; if ($this->showcoursefilter) { $return['filter_selector'] = $this->get_course_filter_selector($output); } if ($context = $this->get_default_add_context()) { $return['defaulteventcontext'] = $context->id; } if ($this->calendar->categoryid) { $return['categoryid'] = $this->calendar->categoryid; } return $return; } /** * 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) { $content = ''; $content .= $output->course_filter_selector($this->url, '', $this->calendar->course->id, $this->calendarinstanceid); return $content; } /** * Get the list of day names for display, re-ordered from the first day * of the week. * * @param renderer_base $output * @return day_name_exporter[] */ protected function get_day_names(renderer_base $output) { $weekdays = $this->related['type']->get_weekdays(); $daysinweek = count($weekdays); $daynames = []; for ($i = 0; $i < $daysinweek; $i++) { // Bump the currentdayno and ensure it loops. $dayno = ($i + $this->firstdayofweek + $daysinweek) % $daysinweek; $dayname = new day_name_exporter($dayno, $weekdays[$dayno]); $daynames[] = $dayname->export($output); } return $daynames; } /** * Get the list of week days, ordered into weeks and padded according * to the value of the first day of the week. * * @param renderer_base $output * @return array The list of weeks. */ protected function get_weeks(renderer_base $output) { $weeks = []; $alldays = $this->get_days(); $daysinweek = count($this->related['type']->get_weekdays()); // Calculate which day number is the first, and last day of the week. $firstdayofweek = $this->firstdayofweek; // The first week is special as it may have padding at the beginning. $day = reset($alldays); $firstdayno = $day['wday']; $prepadding = ($firstdayno + $daysinweek - $firstdayofweek) % $daysinweek; $daysinfirstweek = $daysinweek - $prepadding; $days = array_slice($alldays, 0, $daysinfirstweek); $week = new week_exporter($this->calendar, $days, $prepadding, ($daysinweek - count($days) - $prepadding), $this->related); $weeks[] = $week->export($output); // Now chunk up the remaining day. and turn them into weeks. $daychunks = array_chunk(array_slice($alldays, $daysinfirstweek), $daysinweek); foreach ($daychunks as $days) { $week = new week_exporter($this->calendar, $days, 0, ($daysinweek - count($days)), $this->related); $weeks[] = $week->export($output); } return $weeks; } /** * Get the list of days with the matching date array. * * @return array */ protected function get_days() { $date = $this->related['type']->timestamp_to_date_array($this->calendar->time); $monthdays = $this->related['type']->get_num_days_in_month($date['year'], $date['mon']); $days = []; for ($dayno = 1; $dayno <= $monthdays; $dayno++) { // Get the gregorian representation of the day. $timestamp = $this->related['type']->convert_to_timestamp($date['year'], $date['mon'], $dayno); $days[] = $this->related['type']->timestamp_to_date_array($timestamp); } return $days; } /** * 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 current month timestamp. * * @return int The month timestamp. */ protected function get_month_data() { $date = $this->related['type']->timestamp_to_date_array($this->calendar->time); $monthtime = $this->related['type']->convert_to_gregorian($date['year'], $date['month'], 1); return make_timestamp($monthtime['year'], $monthtime['month']); } /** * Get the previous month timestamp. * * @return int The previous month timestamp. */ protected function get_previous_month_data() { $type = $this->related['type']; $date = $type->timestamp_to_date_array($this->calendar->time); list($date['mon'], $date['year']) = $type->get_prev_month($date['year'], $date['mon']); $time = $type->convert_to_timestamp($date['year'], $date['mon'], 1); return $type->timestamp_to_date_array($time); } /** * Get the next month timestamp. * * @return int The next month timestamp. */ protected function get_next_month_data() { $type = $this->related['type']; $date = $type->timestamp_to_date_array($this->calendar->time); list($date['mon'], $date['year']) = $type->get_next_month($date['year'], $date['mon']); $time = $type->convert_to_timestamp($date['year'], $date['mon'], 1); return $type->timestamp_to_date_array($time); } /** * Set whether the navigation should be shown. * * @param bool $include * @return $this */ public function set_includenavigation($include) { $this->includenavigation = $include; return $this; } /** * Set whether the initial events have already been loaded and * provided to the exporter. * * @param bool $loaded * @return $this */ public function set_initialeventsloaded(bool $loaded) { $this->initialeventsloaded = $loaded; return $this; } /** * Set whether the course filter selector should be shown. * * @param bool $show * @return $this */ public function set_showcoursefilter(bool $show) { $this->showcoursefilter = $show; return $this; } /** * 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; } } PK*\J>Y@@#classes/external/event_exporter.phpnu[. /** * Contains event class for displaying a calendar event. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\external; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . "/calendar/lib.php"); use core_calendar\local\event\container; use core_calendar\output\humantimeperiod; use renderer_base; use core\url; /** * Class for displaying a calendar event. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_exporter extends event_exporter_base { /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { $values = parent::define_other_properties(); $values['url'] = ['type' => PARAM_URL]; return $values; } /** * 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) { $values = parent::get_other_values($output); global $CFG; require_once($CFG->dirroot.'/course/lib.php'); $event = $this->event; $context = $this->related['context']; if ($moduleproxy = $event->get_course_module()) { $modulename = $moduleproxy->get('modname'); $moduleid = $moduleproxy->get('id'); $url = new url(sprintf('/mod/%s/view.php', $modulename), ['id' => $moduleid]); // Build edit event url for action events. $params = array('update' => $moduleid, 'return' => true, 'sesskey' => sesskey()); $editurl = new 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 { $url = \course_get_url($this->related['course'] ?: SITEID); } $values['url'] = $url->out(false); // Override default formatted time to make sure the date portion of the time is always rendered. $legacyevent = container::get_event_mapper()->from_event_to_legacy_event($event); $humanperiod = humantimeperiod::create_from_timestamp( starttimestamp: $legacyevent->timestart, endtimestamp: $legacyevent->timestart + $legacyevent->timeduration, link: new url(CALENDAR_URL . 'view.php'), ); $values['formattedtime'] = $output->render($humanperiod); return $values; } } PK*\' (classes/external/subscription/delete.phpnu[. /** * Calendar external API for deleting the subscription. * * @package core_calendar * @category external * @copyright 2021 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\external\subscription; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/calendar/lib.php'); use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; /** * Calendar external API for deleting the subscription. * * @package core_calendar * @category external * @copyright 2021 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delete extends external_api { /** * Describes the parameters for deleting the subscription. * * @return external_function_parameters * @since Moodle 4.0 */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'subscriptionid' => new external_value(PARAM_INT, 'The id of the subscription', VALUE_REQUIRED) ]); } /** * External function to delete the calendar subscription. * * @param int $subscriptionid Subscription id. * @return array */ public static function execute(int $subscriptionid): array { [ 'subscriptionid' => $subscriptionid ] = self::validate_parameters(self::execute_parameters(), [ 'subscriptionid' => $subscriptionid ]); $status = false; $warnings = []; if (calendar_can_edit_subscription($subscriptionid)) { // Fetch the subscription from the database making sure it exists. $sub = calendar_get_subscription($subscriptionid); calendar_delete_subscription($subscriptionid); $status = true; } else { $warnings = [ 'item' => $subscriptionid, 'warningcode' => 'errordeletingsubscription', 'message' => get_string('nopermissions', 'error') ]; } return [ 'status' => $status, 'warnings' => $warnings ]; } /** * Describes the data returned from the external function. * * @return external_single_structure * @since Moodle 4.0 */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ]); } } PK*\ @''*classes/external/calendar_day_exporter.phpnu[. namespace core_calendar\external; use core\external\exporter; use core_calendar\output\humantimeperiod; use core_date; use DateTimeImmutable; 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 * @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' => 0, ], '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); $moduleinstance = $cache->get_module_instance($event); $exporter = new calendar_event_exporter($event, [ 'context' => $context, 'course' => $course, 'moduleinstance' => $moduleinstance, '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); $humanperiod = humantimeperiod::create_from_timestamp( starttimestamp: $legacyevent->timestart, endtimestamp: $legacyevent->timestart + $legacyevent->timeduration, link: new moodle_url(CALENDAR_URL . 'view.php'), ); $data->formattedtime = $output->render($humanperiod); 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( timezone: core_date::get_user_timezone_object(), ); // The start time should use the day's date but the current // time of the day (adjusted for user's timezone). $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) { return $output->course_filter_selector($this->url, '', $this->calendar->course->id); } /** * 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); } } PK*\gm#m#1classes/external/events_related_objects_cache.phpnu[. /** * Contains event class for providing the related objects when exporting a list of calendar events. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\external; defined('MOODLE_INTERNAL') || die(); use \core_calendar\local\event\entities\event_interface; /** * Class to providing the related objects when exporting a list of calendar events. * * This class is only meant for use with exporters. It attempts to bulk load * the related objects for a list of events and cache them to avoid having * to query the database when exporting each individual event. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class events_related_objects_cache { /** * @var array $events The events for which we need related objects. */ protected $events; /** * @var array $courses The related courses. */ protected $courses = null; /** * @var array $groups The related groups. */ protected $groups = null; /** * @var array $coursemodules The related course modules. */ protected $coursemodules = []; /** * @var array $moduleinstances The related module instances. */ protected $moduleinstances = null; /** * Constructor. * * @param array $events Array of event_interface events * @param array $courses Array of courses to populate the cache with */ public function __construct(array $events, ?array $courses = null) { $this->events = $events; if (!is_null($courses)) { $this->courses = []; foreach ($courses as $course) { $this->courses[$course->id] = $course; } } } /** * Get the related course object for a given event. * * @param event_interface $event The event object. * @return \stdClass|null */ public function get_course(event_interface $event) { if (is_null($this->courses)) { $this->load_courses(); } if ($course = $event->get_course()) { $courseid = $course->get('id'); return isset($this->courses[$courseid]) ? $this->courses[$courseid] : null; } else { return null; } } /** * Get the related context for a given event. * * @param event_interface $event The event object. * @return \context|null */ public function get_context(event_interface $event) { global $USER; $categoryid = $event->get_category() ? $event->get_category()->get('id') : null; $courseid = $event->get_course() ? $event->get_course()->get('id') : null; $groupid = $event->get_group() ? $event->get_group()->get('id') : null; $userid = $event->get_user() ? $event->get_user()->get('id') : null; $moduleid = $event->get_course_module() ? $event->get_course_module()->get('id') : null; if (!empty($categoryid)) { return \context_coursecat::instance($categoryid); } else if (!empty($courseid)) { return \context_course::instance($event->get_course()->get('id')); } else if (!empty($groupid)) { $group = $this->get_group($event); return \context_course::instance($group->courseid); } else if (!empty($userid) && $userid == $USER->id) { return \context_user::instance($userid); } else if (!empty($userid) && $userid != $USER->id && $moduleid && $moduleid > 0) { $cm = $this->get_course_module($event); return \context_course::instance($cm->course); } else { return \context_user::instance($userid); } } /** * Get the related group object for a given event. * * @param event_interface $event The event object. * @return \stdClass|null */ public function get_group(event_interface $event) { if (is_null($this->groups)) { $this->load_groups(); } if ($group = $event->get_group()) { $groupid = $group->get('id'); return isset($this->groups[$groupid]) ? $this->groups[$groupid] : null; } else { return null; } } /** * Get the related course module for a given event. * * @param event_interface $event The event object. * @return \stdClass|null */ public function get_course_module(event_interface $event) { if (!$event->get_course_module()) { return null; } $id = $event->get_course_module()->get('id'); $name = $event->get_course_module()->get('modname'); $key = $name . '_' . $id; if (!isset($this->coursemodules[$key])) { $this->coursemodules[$key] = get_coursemodule_from_instance($name, $id, 0, false, MUST_EXIST); } return $this->coursemodules[$key]; } /** * Get the related module instance for a given event. * * @param event_interface $event The event object. * @return \stdClass|null */ public function get_module_instance(event_interface $event) { if (!$event->get_course_module()) { return null; } if (is_null($this->moduleinstances)) { $this->load_module_instances(); } $id = $event->get_course_module()->get('instance'); $name = $event->get_course_module()->get('modname'); if (isset($this->moduleinstances[$name])) { if (isset($this->moduleinstances[$name][$id])) { return $this->moduleinstances[$name][$id]; } } return null; } /** * Load the list of all of the distinct courses required for the * list of provided events and save the result in memory. */ protected function load_courses() { global $DB; $courseids = []; foreach ($this->events as $event) { if ($course = $event->get_course()) { $id = $course->get('id'); $courseids[$id] = true; } } if (empty($courseids)) { $this->courses = []; return; } list($idsql, $params) = $DB->get_in_or_equal(array_keys($courseids)); $sql = "SELECT * FROM {course} WHERE id {$idsql}"; $this->courses = $DB->get_records_sql($sql, $params); } /** * Load the list of all of the distinct groups required for the * list of provided events and save the result in memory. */ protected function load_groups() { global $DB; $groupids = []; foreach ($this->events as $event) { if ($group = $event->get_group()) { $id = $group->get('id'); $groupids[$id] = true; } } if (empty($groupids)) { $this->groups = []; return; } list($idsql, $params) = $DB->get_in_or_equal(array_keys($groupids)); $sql = "SELECT * FROM {groups} WHERE id {$idsql}"; $this->groups = $DB->get_records_sql($sql, $params); } /** * Load the list of all of the distinct module instances required for the * list of provided events and save the result in memory. */ protected function load_module_instances() { global $DB; $this->moduleinstances = []; $modulestoload = []; foreach ($this->events as $event) { if ($module = $event->get_course_module()) { $id = $module->get('instance'); $name = $module->get('modname'); $ids = isset($modulestoload[$name]) ? $modulestoload[$name] : []; $ids[$id] = true; $modulestoload[$name] = $ids; } } if (empty($modulestoload)) { return; } foreach ($modulestoload as $modulename => $ids) { list($idsql, $params) = $DB->get_in_or_equal(array_keys($ids)); $sql = "SELECT * FROM {" . $modulename . "} WHERE id {$idsql}"; $this->moduleinstances[$modulename] = $DB->get_records_sql($sql, $params); } } } PK*\~0==(classes/external/event_exporter_base.phpnu[. /** * Contains event class for displaying a calendar event. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\external; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . "/calendar/lib.php"); require_once($CFG->libdir . "/filelib.php"); use core\external\exporter; use core_calendar\local\event\container; use core_calendar\local\event\entities\event_interface; use core_calendar\local\event\entities\action_event_interface; use core_calendar\output\humantimeperiod; use core_course\external\course_summary_exporter; use core\external\coursecat_summary_exporter; use renderer_base; use core\url; /** * Class for displaying a calendar event. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_exporter_base extends exporter { /** * @var event_interface $event */ protected $event; /** * Constructor. * * @param event_interface $event * @param array $related The related data. */ public function __construct(event_interface $event, $related = []) { $this->event = $event; $starttimestamp = $event->get_times()->get_start_time()->getTimestamp(); $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->name = clean_string($event->get_name()); $data->description = file_rewrite_pluginfile_urls( $event->get_description()->get_value(), 'pluginfile.php', $related['context']->id, 'calendar', 'event_description', $event->get_id() ); $data->descriptionformat = $event->get_description()->get_format(); $data->location = \core_external\util::format_text($event->get_location(), FORMAT_PLAIN, $related['context']->id)[0]; $data->groupid = $groupid; $data->userid = $userid; $data->categoryid = $categoryid; $data->eventtype = $event->get_type(); $data->timestart = $starttimestamp; $data->timeduration = $endtimestamp - $starttimestamp; $data->timesort = $event->get_times()->get_sort_time()->getTimestamp(); $data->timeusermidnight = $event->get_times()->get_usermidnight_time()->getTimestamp(); $data->visible = $event->is_visible() ? 1 : 0; $data->timemodified = $event->get_times()->get_modified_time()->getTimestamp(); $data->component = $event->get_component(); $data->overdue = $data->timesort < time(); if ($repeats = $event->get_repeats()) { $data->repeatid = $repeats->get_id(); $data->eventcount = $repeats->get_num() + 1; } if ($cm = $event->get_course_module()) { $data->modulename = $cm->get('modname'); $data->instance = $cm->get('id'); $data->activityname = $cm->get('name'); $component = 'mod_' . $data->modulename; if (!component_callback_exists($component, 'core_calendar_get_event_action_string')) { $modulename = get_string('modulename', $data->modulename); $data->activitystr = get_string('requiresaction', 'calendar', $modulename); } else { $data->activitystr = component_callback( $component, 'core_calendar_get_event_action_string', [$event->get_type()] ); } } parent::__construct($data, $related); } /** * Return the list of properties. * * @return array */ protected static function define_properties() { return [ 'id' => ['type' => PARAM_INT], 'name' => ['type' => PARAM_TEXT], 'description' => [ 'type' => PARAM_RAW, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'descriptionformat' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'location' => [ 'type' => PARAM_RAW, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'categoryid' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'groupid' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'userid' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'repeatid' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'eventcount' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'component' => [ 'type' => PARAM_COMPONENT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'modulename' => [ 'type' => PARAM_TEXT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'activityname' => [ 'type' => PARAM_TEXT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'activitystr' => [ 'type' => PARAM_TEXT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'instance' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'eventtype' => ['type' => PARAM_TEXT], 'timestart' => ['type' => PARAM_INT], 'timeduration' => ['type' => PARAM_INT], 'timesort' => ['type' => PARAM_INT], 'timeusermidnight' => ['type' => PARAM_INT], 'visible' => ['type' => PARAM_INT], 'timemodified' => ['type' => PARAM_INT], 'overdue' => [ 'type' => PARAM_BOOL, 'optional' => true, 'default' => false, 'null' => NULL_ALLOWED ], ]; } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ '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, ], 'subscription' => [ 'type' => event_subscription_exporter::read_properties_definition(), 'optional' => true, ], 'canedit' => [ 'type' => PARAM_BOOL ], 'candelete' => [ 'type' => PARAM_BOOL ], 'deleteurl' => [ 'type' => PARAM_URL ], 'editurl' => [ 'type' => PARAM_URL ], 'viewurl' => [ 'type' => PARAM_URL ], 'formattedtime' => [ 'type' => PARAM_RAW, ], 'formattedlocation' => [ 'type' => PARAM_RAW, ], 'isactionevent' => [ 'type' => PARAM_BOOL ], 'iscourseevent' => [ 'type' => PARAM_BOOL ], 'iscategoryevent' => [ 'type' => PARAM_BOOL ], 'groupname' => [ 'type' => PARAM_RAW, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'normalisedeventtype' => [ 'type' => PARAM_TEXT ], 'normalisedeventtypetext' => [ 'type' => PARAM_TEXT ], 'action' => [ 'type' => event_action_exporter::read_properties_definition(), 'optional' => true, ], 'purpose' => [ 'type' => PARAM_TEXT ], 'branded' => [ 'type' => PARAM_BOOL, 'optional' => true, ], ]; } /** * 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) { $values = []; $event = $this->event; $legacyevent = container::get_event_mapper()->from_event_to_legacy_event($event); $context = $this->related['context']; $course = $this->related['course']; $values['isactionevent'] = false; $values['iscourseevent'] = false; $values['iscategoryevent'] = false; $values['normalisedeventtype'] = $event->get_type(); if ($moduleproxy = $event->get_course_module()) { // We need a separate property to flag if an event is action event. // That's required because canedit return true but action action events cannot be edited on the calendar UI. // But they are considered editable because you can drag and drop the event on the month view. $values['isactionevent'] = true; // Activity events are normalised to "look" like course events. $values['normalisedeventtype'] = 'course'; } 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]); $identifier = 'type' . $values['normalisedeventtype']; $stringexists = get_string_manager()->string_exists($identifier, 'calendar'); if (!$stringexists) { // Property normalisedeventtype is used to build the name of the CSS class for the events. $values['normalisedeventtype'] = 'other'; } $values['normalisedeventtypetext'] = $stringexists ? get_string($identifier, 'calendar') : ''; $purpose = 'none'; $isbranded = false; if ($moduleproxy) { $purpose = plugin_supports('mod', $moduleproxy->get('modname'), FEATURE_MOD_PURPOSE, 'none'); $isbranded = component_callback('mod_' . $moduleproxy->get('modname'), 'is_branded') !== null ? : false; } $values['purpose'] = $purpose; $values['branded'] = $isbranded; $values['icon'] = $iconexporter->export($output); $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 && $course->id != SITEID) { $coursesummaryexporter = new course_summary_exporter($course, ['context' => $context]); $values['course'] = $coursesummaryexporter->export($output); } $courseid = (!$course) ? SITEID : $course->id; $values['canedit'] = calendar_edit_event_allowed($legacyevent, true); $values['candelete'] = calendar_delete_event_allowed($legacyevent); $deleteurl = new url('/calendar/delete.php', ['id' => $event->get_id(), 'course' => $courseid]); $values['deleteurl'] = $deleteurl->out(false); $editurl = new url('/calendar/event.php', ['action' => 'edit', 'id' => $event->get_id(), 'course' => $courseid]); $values['editurl'] = $editurl->out(false); $viewurl = new url('/calendar/view.php', ['view' => 'day', 'course' => $courseid, 'time' => $timesort]); $viewurl->set_anchor('event_' . $event->get_id()); $values['viewurl'] = $viewurl->out(false); $legacyevent = container::get_event_mapper()->from_event_to_legacy_event($event); $humanperiod = humantimeperiod::create_from_timestamp( starttimestamp: $legacyevent->timestart, endtimestamp: $legacyevent->timestart + $legacyevent->timeduration, link: new url(CALENDAR_URL . 'view.php'), ); $values['formattedtime'] = $output->render($humanperiod); $values['formattedlocation'] = calendar_format_event_location($legacyevent); if ($group = $event->get_group()) { $values['groupname'] = format_string($group->get('name'), true, ['context' => \context_course::instance($event->get_course()->get('id'))]); } if ($event instanceof action_event_interface) { // Export event action if applicable. $actionrelated = [ 'context' => $this->related['context'], 'event' => $event ]; $actionexporter = new event_action_exporter($event->get_action(), $actionrelated); $values['action'] = $actionexporter->export($output); } return $values; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'context' => 'context', 'course' => 'stdClass?', ]; } } PK*\GM M !classes/external/export/token.phpnu[. /** * This is the external method for exporting a calendar token. * * @package core_calendar * @since Moodle 3.10 * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\external\export; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); use context_system; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use moodle_exception; /** * This is the external method for exporting a calendar token. * * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class token extends external_api { /** * Returns description of method parameters. * * @return external_function_parameters. * @since Moodle 3.10 */ public static function execute_parameters() { return new external_function_parameters([]); } /** * Return the auth token required for exporting a calendar. * * @return array The access information * @throws moodle_exception * @since Moodle 3.10 */ public static function execute() { global $CFG, $USER; $context = context_system::instance(); self::validate_context($context); if (empty($CFG->enablecalendarexport)) { throw new moodle_exception('Calendar export is disabled in this site.'); } return [ 'token' => calendar_get_export_token($USER), 'warnings' => [], ]; } /** * Returns description of method result value. * * @return \core_external\external_description. * @since Moodle 3.10 */ public static function execute_returns() { return new external_single_structure( [ 'token' => new external_value(PARAM_RAW, 'The calendar permanent access token for calendar export.'), 'warnings' => new external_warnings(), ] ); } } PK*\% "classes/external/week_exporter.phpnu[. /** * Contains event class for displaying the week view. * * @package core_calendar * @copyright 2017 Andrew Nicols * @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; /** * Class for displaying the week view. * * @package core_calendar * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class week_exporter extends exporter { /** * @var array $days An array of day_exporter objects. */ protected $days = []; /** * @var int $prepadding The number of pre-padding days at the start of the week. */ protected $prepadding = 0; /** * @var int $postpadding The number of post-padding days at the start of the week. */ protected $postpadding = 0; /** * @var \calendar_information $calendar The calendar being displayed. */ protected $calendar; /** * Constructor. * * @param \calendar_information $calendar The calendar information for the period being displayed * @param mixed $days An array of day_exporter objects. * @param int $prepadding The number of pre-padding days at the start of the week. * @param int $postpadding The number of post-padding days at the start of the week. * @param array $related Related objects. */ public function __construct(\calendar_information $calendar, $days, $prepadding, $postpadding, $related) { $this->days = $days; $this->prepadding = $prepadding; $this->postpadding = $postpadding; $this->calendar = $calendar; parent::__construct([], $related); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'prepadding' => [ 'type' => PARAM_INT, 'multiple' => true, ], 'postpadding' => [ 'type' => PARAM_INT, 'multiple' => true, ], 'days' => [ 'type' => week_day_exporter::read_properties_definition(), 'multiple' => true, ], ]; } /** * 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) { global $CFG; $return = [ 'prepadding' => [], 'postpadding' => [], 'days' => [], ]; for ($i = 0; $i < $this->prepadding; $i++) { $return['prepadding'][] = $i; } for ($i = 0; $i < $this->postpadding; $i++) { $return['postpadding'][] = $i; } $return['days'] = []; $today = $this->related['type']->timestamp_to_date_array(time()); $weekend = CALENDAR_DEFAULT_WEEKEND; if (isset($CFG->calendar_weekend)) { $weekend = intval($CFG->calendar_weekend); } $numberofdaysinweek = $this->related['type']->get_num_weekdays(); foreach ($this->days as $daydata) { $events = []; foreach ($this->related['events'] as $event) { $times = $event->get_times(); $starttime = $times->get_start_time()->getTimestamp(); $startdate = $this->related['type']->timestamp_to_date_array($starttime); $endtime = $times->get_end_time()->getTimestamp(); $enddate = $this->related['type']->timestamp_to_date_array($endtime); if ((($startdate['year'] * 366) + $startdate['yday']) > ($daydata['year'] * 366) + $daydata['yday']) { // Starts after today. continue; } if ((($enddate['year'] * 366) + $enddate['yday']) < ($daydata['year'] * 366) + $daydata['yday']) { // Ends before today. continue; } $events[] = $event; } $istoday = true; $istoday = $istoday && $today['year'] == $daydata['year']; $istoday = $istoday && $today['yday'] == $daydata['yday']; $daydata['istoday'] = $istoday; $daydata['isweekend'] = !!($weekend & (1 << ($daydata['wday'] % $numberofdaysinweek))); $day = new week_day_exporter($this->calendar, $daydata, [ 'events' => $events, 'cache' => $this->related['cache'], 'type' => $this->related['type'], ]); $return['days'][] = $day->export($output); } return $return; } /** * 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', ]; } } PK*\1hh(classes/external/event_icon_exporter.phpnu[. /** * Contains event class for displaying a calendar event's icon. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @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 \core_calendar\local\event\entities\event_interface; /** * Class for displaying a calendar event's icon. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_icon_exporter extends exporter { /** * Constructor. * * @param event_interface $event * @param array $related The related data. */ public function __construct(event_interface $event, $related = []) { global $PAGE; $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(); $groupid = $group ? $group->get('id') : null; $user = $event->get_user(); $userid = $user ? $user->get('id') : null; $isactivityevent = !empty($coursemodule); $issiteevent = ($course && $courseid == SITEID); $iscategoryevent = ($category && !empty($categoryid)); $iscourseevent = ($course && !empty($courseid) && $courseid != SITEID && empty($groupid)); $isgroupevent = ($group && !empty($groupid)); $isuserevent = ($user && !empty($userid)); $iconurl = ''; $iconclass = ''; if ($isactivityevent) { $key = 'monologo'; $component = $coursemodule->get('modname'); $iconurl = get_fast_modinfo($courseid)->get_cm($coursemodule->get('id'))->get_icon_url(); $iconclass = $iconurl->get_param('filtericon') ? '' : 'nofilter'; $iconurl = $iconurl->out(false); if (get_string_manager()->string_exists($event->get_type(), $component)) { $alttext = get_string($event->get_type(), $component); } else { $alttext = get_string('activityevent', 'calendar'); } } else if ($event->get_component()) { // Guess the icon and the title for the component event. By default display calendar icon and the // plugin name as the alttext. if ($PAGE->theme->resolve_image_location($event->get_type(), $event->get_component())) { $key = $event->get_type(); $component = $event->get_component(); } else { $key = 'i/otherevent'; $component = 'core'; } if (get_string_manager()->string_exists($event->get_type(), $event->get_component())) { $alttext = get_string($event->get_type(), $event->get_component()); } else { $alttext = get_string('pluginname', $event->get_component()); } } else if ($issiteevent) { $key = 'i/siteevent'; $component = 'core'; $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('typecourse', 'calendar'); } else if ($isgroupevent) { $key = 'i/groupevent'; $component = 'core'; $alttext = get_string('typegroup', 'calendar'); } else if ($isuserevent) { $key = 'i/userevent'; $component = 'core'; $alttext = get_string('typeuser', 'calendar'); } else { // Default to site event icon? $key = 'i/siteevent'; $component = 'core'; $alttext = get_string('typesite', 'calendar'); } $data = new \stdClass(); $data->key = $key; $data->component = $component; $data->alttext = $alttext; $data->iconurl = $iconurl; $data->iconclass = $iconclass; parent::__construct($data, $related); } /** * Return the list of properties. * * @return array */ protected static function define_properties() { return [ 'key' => ['type' => PARAM_TEXT], 'component' => ['type' => PARAM_TEXT], 'alttext' => ['type' => PARAM_TEXT], 'iconurl' => ['type' => PARAM_TEXT], 'iconclass' => ['type' => PARAM_TEXT], ]; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'context' => 'context', ]; } } PK*\s)w) 6classes/external/events_grouped_by_course_exporter.phpnu[. /** * Contains event class for displaying a list of calendar events grouped by course id. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @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; /** * Class for displaying a list of calendar events grouped by course id. * * This class uses the events relateds cache in order to get the related * data for exporting an event without having to naively hit the database * for each event. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class events_grouped_by_course_exporter extends exporter { /** * @var array $events An array of event_interface objects * grouped and index by course id. */ protected $eventsbycourse; /** * Constructor. * * @param array $eventsbycourse An array of event_interface objects * @param array $related An array of related objects */ public function __construct(array $eventsbycourse, $related = []) { $this->eventsbycourse = $eventsbycourse; parent::__construct([], $related); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'groupedbycourse' => [ 'type' => events_same_course_exporter::read_properties_definition(), 'multiple' => true, 'default' => [], ], ]; } /** * 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) { $return = []; $cache = $this->related['cache']; foreach ($this->eventsbycourse as $courseid => $events) { $eventsexporter = new events_same_course_exporter( $courseid, $events, ['cache' => $cache]); $return['groupedbycourse'][] = $eventsexporter->export($output); } return $return; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'cache' => 'core_calendar\external\events_related_objects_cache', ]; } } PK*\IH<<,classes/external/calendar_event_exporter.phpnu[. /** * Contains event class for displaying a calendar event. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\external; defined('MOODLE_INTERNAL') || die(); use \core_calendar\local\event\container; use \renderer_base; require_once($CFG->dirroot . '/course/lib.php'); /** * Class for displaying a calendar event. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class calendar_event_exporter extends event_exporter_base { /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { $values = parent::define_other_properties(); $values['url'] = ['type' => PARAM_URL]; $values['islastday'] = [ 'type' => PARAM_BOOL, 'default' => false, ]; $values['popupname'] = [ 'type' => PARAM_RAW, ]; $values['mindaytimestamp'] = [ 'type' => PARAM_INT, 'optional' => true ]; $values['mindayerror'] = [ 'type' => PARAM_TEXT, 'optional' => true ]; $values['maxdaytimestamp'] = [ 'type' => PARAM_INT, 'optional' => true ]; $values['maxdayerror'] = [ 'type' => PARAM_TEXT, 'optional' => true ]; $values['draggable'] = [ 'type' => PARAM_BOOL, 'default' => false ]; return $values; } /** * 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) { global $CFG; $values = parent::get_other_values($output); $event = $this->event; $course = $this->related['course']; $hascourse = !empty($course); // By default all events that can be edited are // draggable. $values['draggable'] = $values['canedit']; if ($moduleproxy = $event->get_course_module()) { $modulename = $moduleproxy->get('modname'); $moduleid = $moduleproxy->get('id'); $url = new \moodle_url(sprintf('/mod/%s/view.php', $modulename), ['id' => $moduleid]); // Build edit event url for action events. $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 { $url = course_get_url($hascourse ? $course : SITEID); } $values['url'] = $url->out(false); $values['islastday'] = false; $today = $this->related['type']->timestamp_to_date_array($this->related['today']); if ($hascourse) { $values['popupname'] = \core_external\util::format_string( $this->event->get_name(), \context_course::instance($course->id), true ); } else { $values['popupname'] = \core_external\util::format_string($this->event->get_name(), \context_system::instance(), true); } $times = $this->event->get_times(); if ($duration = $times->get_duration()) { $enddate = $this->related['type']->timestamp_to_date_array($times->get_end_time()->getTimestamp()); $values['islastday'] = true; $values['islastday'] = $values['islastday'] && $enddate['year'] == $today['year']; $values['islastday'] = $values['islastday'] && $enddate['mon'] == $today['mon']; $values['islastday'] = $values['islastday'] && $enddate['mday'] == $today['mday']; } $subscription = $this->event->get_subscription(); if ($subscription && !empty($subscription->get('id')) && $CFG->calendar_showicalsource) { $a = (object) [ 'name' => $values['popupname'], 'source' => $subscription->get('name'), ]; $values['popupname'] = get_string('namewithsource', 'calendar', $a); } else { if ($values['islastday']) { $startdate = $this->related['type']->timestamp_to_date_array($times->get_start_time()->getTimestamp()); $samedate = true; $samedate = $samedate && $startdate['mon'] == $enddate['mon']; $samedate = $samedate && $startdate['year'] == $enddate['year']; $samedate = $samedate && $startdate['mday'] == $enddate['mday']; if (!$samedate) { $values['popupname'] = get_string('eventendtimewrapped', 'calendar', $values['popupname']); } } } // 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. if ($hascourse && $course->id !== SITEID) { $eventnameparams = (object) [ 'name' => $values['popupname'], 'course' => $values['course']->shortname, ]; $values['popupname'] = get_string('eventnameandcourse', 'calendar', $eventnameparams); } if ($event->get_course_module()) { $values = array_merge($values, $this->get_module_timestamp_limits($event)); } else if ($hascourse && $course->id != SITEID && empty($event->get_group())) { // This is a course event. $values = array_merge($values, $this->get_course_timestamp_limits($event)); } return $values; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { $related = parent::define_related(); $related['daylink'] = \moodle_url::class; $related['type'] = '\core_calendar\type_base'; $related['today'] = 'int'; $related['moduleinstance'] = 'stdClass?'; return $related; } /** * Return the normalised event type. * Activity events are normalised to be course events. * * @return string */ public function get_calendar_event_type() { if ($this->event->get_course_module()) { return 'course'; } return $this->event->get_type(); } /** * Return the set of minimum and maximum date timestamp values * for the given event. * * @param \core_calendar\local\event\entities\event_interface $event * @return array */ protected function get_course_timestamp_limits($event) { $values = []; $mapper = container::get_event_mapper(); $starttime = $event->get_times()->get_start_time(); list($min, $max) = component_callback( 'core_course', 'core_calendar_get_valid_event_timestart_range', [$mapper->from_event_to_legacy_event($event), $event->get_course()->get_proxied_instance()], [false, false] ); // The callback will return false for either of the // min or max cutoffs to indicate that there are no // valid timestart values. In which case the event is // not draggable. if ($min === false || $max === false) { return ['draggable' => false]; } if ($min) { $values = array_merge($values, $this->get_timestamp_min_limit($starttime, $min)); } if ($max) { $values = array_merge($values, $this->get_timestamp_max_limit($starttime, $max)); } return $values; } /** * Return the set of minimum and maximum date timestamp values * for the given event. * * @param \core_calendar\local\event\entities\event_interface $event * @return array */ protected function get_module_timestamp_limits($event) { $values = []; $mapper = container::get_event_mapper(); $starttime = $event->get_times()->get_start_time(); $modname = $event->get_course_module()->get('modname'); $moduleinstance = $this->related['moduleinstance']; list($min, $max) = component_callback( 'mod_' . $modname, 'core_calendar_get_valid_event_timestart_range', [$mapper->from_event_to_legacy_event($event), $moduleinstance], [false, false] ); // The callback will return false for either of the // min or max cutoffs to indicate that there are no // valid timestart values. In which case the event is // not draggable. if ($min === false || $max === false) { return ['draggable' => false]; } if ($min) { $values = array_merge($values, $this->get_timestamp_min_limit($starttime, $min)); } if ($max) { $values = array_merge($values, $this->get_timestamp_max_limit($starttime, $max)); } return $values; } /** * Get the correct minimum midnight day limit based on the event start time * and the minimum timestamp limit of what the event belongs to. * * @param DateTimeInterface $starttime The event start time * @param array $min The module's minimum limit for the event * @return array Returns an array with mindaytimestamp and mindayerror keys. */ protected function get_timestamp_min_limit(\DateTimeInterface $starttime, $min) { // We need to check that the minimum valid time is earlier in the // day than the current event time so that if the user drags and drops // the event to this day (which changes the date but not the time) it // will result in a valid time start for the event. // // For example: // An event that starts on 2017-01-10 08:00 with a minimum cutoff // of 2017-01-05 09:00 means that 2017-01-05 is not a valid start day // for the drag and drop because it would result in the event start time // being set to 2017-01-05 08:00, which is invalid. Instead the minimum // valid start day would be 2017-01-06. $values = []; $timestamp = $min[0]; $errorstring = $min[1]; $mindate = (new \DateTimeImmutable())->setTimestamp($timestamp); $minstart = $mindate->setTime( $starttime->format('H'), $starttime->format('i'), $starttime->format('s') ); $midnight = usergetmidnight($timestamp); if ($mindate <= $minstart) { $values['mindaytimestamp'] = $midnight; } else { $tomorrow = (new \DateTime())->setTimestamp($midnight)->modify('+1 day'); $values['mindaytimestamp'] = $tomorrow->getTimestamp(); } // Get the human readable error message to display if the min day // timestamp is violated. $values['mindayerror'] = $errorstring; return $values; } /** * Get the correct maximum midnight day limit based on the event start time * and the maximum timestamp limit of what the event belongs to. * * @param DateTimeInterface $starttime The event start time * @param array $max The module's maximum limit for the event * @return array Returns an array with maxdaytimestamp and maxdayerror keys. */ protected function get_timestamp_max_limit(\DateTimeInterface $starttime, $max) { // We're doing a similar calculation here as we are for the minimum // day timestamp. See the explanation above. $values = []; $timestamp = $max[0]; $errorstring = $max[1]; $maxdate = (new \DateTimeImmutable())->setTimestamp($timestamp); $maxstart = $maxdate->setTime( $starttime->format('H'), $starttime->format('i'), $starttime->format('s') ); $midnight = usergetmidnight($timestamp); if ($maxdate >= $maxstart) { $values['maxdaytimestamp'] = $midnight; } else { $yesterday = (new \DateTime())->setTimestamp($midnight)->modify('-1 day'); $values['maxdaytimestamp'] = $yesterday->getTimestamp(); } // Get the human readable error message to display if the max day // timestamp is violated. $values['maxdayerror'] = $errorstring; return $values; } /** * Get the correct minimum midnight day limit based on the event start time * and the module's minimum timestamp limit. * * @deprecated since Moodle 3.6. Please use get_timestamp_min_limit(). * @todo final deprecation. To be removed in Moodle 3.10 * @param \DateTimeInterface $starttime The event start time * @param array $min The module's minimum limit for the event * @return array Returns an array with mindaytimestamp and mindayerror keys. */ protected function get_module_timestamp_min_limit(\DateTimeInterface $starttime, $min) { debugging('get_module_timestamp_min_limit() has been deprecated. Please call get_timestamp_min_limit() instead.', DEBUG_DEVELOPER); return $this->get_timestamp_min_limit($starttime, $min); } /** * Get the correct maximum midnight day limit based on the event start time * and the module's maximum timestamp limit. * * @deprecated since Moodle 3.6. Please use get_timestamp_max_limit(). * @todo final deprecation. To be removed in Moodle 3.10 * @param \DateTimeInterface $starttime The event start time * @param array $max The module's maximum limit for the event * @return array Returns an array with maxdaytimestamp and maxdayerror keys. */ protected function get_module_timestamp_max_limit(\DateTimeInterface $starttime, $max) { debugging('get_module_timestamp_max_limit() has been deprecated. Please call get_timestamp_max_limit() instead.', DEBUG_DEVELOPER); return $this->get_timestamp_max_limit($starttime, $max); } } PK*\^ܧtt/classes/external/calendar_upcoming_exporter.phpnu[. /** * Contains event class for displaying the upcoming view. * * @package core_calendar * @copyright 2017 Simey Lameze * @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 core_calendar\output\humantimeperiod; use renderer_base; use core\url; use core_calendar\local\event\container; /** * Class for displaying the day view. * * @package core_calendar * @copyright 2017 Simey Lameze * @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 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' => 0, ], 'filter_selector' => [ 'type' => PARAM_RAW, ], 'courseid' => [ 'type' => PARAM_INT, ], 'categoryid' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => 0, ], 'isloggedin' => [ 'type' => PARAM_BOOL, ], 'date' => [ 'type' => date_exporter::read_properties_definition(), ], ]; } /** * 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 url('/calendar/view.php', [ 'view' => 'upcoming', 'time' => $timestamp, 'course' => $this->calendar->course->id, ]); $this->url = $url; $return['isloggedin'] = isloggedin(); $return['events'] = array_map(function($event) use ($cache, $output, $url) { $context = $cache->get_context($event); $course = $cache->get_course($event); $moduleinstance = $cache->get_module_instance($event); $exporter = new calendar_event_exporter($event, [ 'context' => $context, 'course' => $course, 'moduleinstance' => $moduleinstance, '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); $humanperiod = humantimeperiod::create_from_timestamp( starttimestamp: $legacyevent->timestart, endtimestamp: $legacyevent->timestart + $legacyevent->timeduration, link: new url(CALENDAR_URL . 'view.php'), ); $data->formattedtime = $output->render($humanperiod); 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; $date = $this->related['type']->timestamp_to_date_array($this->calendar->time); $return['date'] = (new date_exporter($date))->export($output); if ($this->calendar->categoryid) { $return['categoryid'] = $this->calendar->categoryid; } 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) { return $output->course_filter_selector($this->url, '', $this->calendar->course->id); } /** * 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', ]; } } PK*\ì&classes/external/week_day_exporter.phpnu[. /** * Contains event class for displaying the day on month view. * * @package core_calendar * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\external; defined('MOODLE_INTERNAL') || die(); use renderer_base; /** * Class for displaying the day on month view. * * @package core_calendar * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class week_day_exporter extends day_exporter { /** * Constructor. * * @param \calendar_information $calendar The calendar information for the period being displayed * @param mixed $data Either an stdClass or an array of values. * @param array $related Related objects. */ public function __construct(\calendar_information $calendar, $data, $related) { parent::__construct($calendar, $data, $related); // Fix the url for today to be based on the today timestamp // rather than the calendar_information time set in the parent // constructor. $this->url->param('time', $this->data[0]); } /** * Return the list of properties. * * @return array */ protected static function define_properties() { $return = parent::define_properties(); $return = array_merge($return, [ // These are additional params. 'istoday' => [ 'type' => PARAM_BOOL, 'default' => false, ], 'isweekend' => [ 'type' => PARAM_BOOL, 'default' => false, ], ]); return $return; } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { $return = parent::define_other_properties(); $return = array_merge($return, [ 'popovertitle' => [ 'type' => PARAM_RAW, 'default' => '', ], 'daytitle' => [ 'type' => PARAM_RAW, ] ]); return $return; } /** * 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) { $return = parent::get_other_values($output); if ($popovertitle = $this->get_popover_title()) { $return['popovertitle'] = $popovertitle; } $return['daytitle'] = $this->get_day_title(); return $return; } /** * 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 title for this popover. * * @return string */ protected function get_popover_title() { $title = null; $userdate = userdate($this->data[0], get_string('strftimedayshort')); if (count($this->related['events'])) { $title = get_string('eventsfor', 'calendar', $userdate); } else if ($this->data['istoday']) { $title = $userdate; } if ($this->data['istoday']) { $title = get_string('todayplustitle', 'calendar', $userdate); } return $title; } /** * Get the title for this day. * * @return string */ protected function get_day_title(): string { $userdate = userdate($this->data[0], get_string('strftimedayshort')); $numevents = count($this->related['events']); if ($numevents == 1) { $title = get_string('dayeventsone', 'calendar', $userdate); } else if ($numevents) { $title = get_string('dayeventsmany', 'calendar', ['num' => $numevents, 'day' => $userdate]); } else { $title = get_string('dayeventsnone', 'calendar', $userdate); } return $title; } } PK*\S "classes/external/date_exporter.phpnu[. /** * Class for normalising the date data. * * @package core_calendar * @copyright 2017 Andrew Nicols * @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; /** * Class for normalising the date data. * * @package core_calendar * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class date_exporter extends exporter { /** * Constructor for date_exporter. * * @param array $data * @param array $related The related information */ public function __construct($data, $related = []) { $data['timestamp'] = $data[0]; unset($data[0]); parent::__construct($data, $related); } protected static function define_properties() { return [ 'seconds' => [ 'type' => PARAM_INT, ], 'minutes' => [ 'type' => PARAM_INT, ], 'hours' => [ 'type' => PARAM_INT, ], 'mday' => [ 'type' => PARAM_INT, ], 'wday' => [ 'type' => PARAM_INT, ], 'mon' => [ 'type' => PARAM_INT, ], 'year' => [ 'type' => PARAM_INT, ], 'yday' => [ 'type' => PARAM_INT, ], 'weekday' => [ 'type' => PARAM_RAW, ], 'month' => [ 'type' => PARAM_RAW, ], 'timestamp' => [ 'type' => PARAM_INT, ], ]; } } PK*\Xagh 0classes/external/events_same_course_exporter.phpnu[. /** * Contains event class for displaying a list of calendar events for a single course. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\external; defined('MOODLE_INTERNAL') || die(); use \renderer_base; /** * Class for displaying a list of calendar events for a single course. * * This class uses the events relateds cache in order to get the related * data for exporting an event without having to naively hit the database * for each event. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class events_same_course_exporter extends events_exporter { /** * @var array $courseid The id of the course for these events. */ protected $courseid; /** * Constructor. * * @param int $courseid The course id for these events * @param array $events An array of event_interface objects * @param array $related An array of related objects */ public function __construct($courseid, array $events, $related = []) { parent::__construct($events, $related); $this->courseid = $courseid; } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { $properties = parent::define_other_properties(); $properties['courseid'] = ['type' => PARAM_INT]; return $properties; } /** * 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) { $values = parent::get_other_values($output); $values['courseid'] = $this->courseid; return $values; } } PK*\Odd$classes/external/events_exporter.phpnu[. /** * Contains event class for displaying a list of calendar events. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @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; /** * Class for displaying a list of calendar events. * * This class uses the events relateds cache in order to get the related * data for exporting an event without having to naively hit the database * for each event. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class events_exporter extends exporter { /** * @var array $events An array of event_interface objects. */ protected $events; /** * Constructor. * * @param array $events An array of event_interface objects * @param array $related An array of related objects */ public function __construct(array $events, $related = []) { $this->events = $events; parent::__construct([], $related); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'events' => [ 'type' => event_exporter::read_properties_definition(), 'multiple' => true, ], 'firstid' => [ 'type' => PARAM_INT, 'null' => NULL_ALLOWED, 'default' => null, ], 'lastid' => [ 'type' => PARAM_INT, 'null' => NULL_ALLOWED, 'default' => null, ], ]; } /** * 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) { $return = []; $cache = $this->related['cache']; $return['events'] = array_map(function($event) use ($cache, $output) { $context = $cache->get_context($event); $course = $cache->get_course($event); $exporter = new event_exporter($event, ['context' => $context, 'course' => $course]); return $exporter->export($output); }, $this->events); if ($count = count($return['events'])) { $return['firstid'] = $return['events'][0]->id; $return['lastid'] = $return['events'][$count - 1]->id; } return $return; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'cache' => 'core_calendar\external\events_related_objects_cache', ]; } } PK*\HJrr*classes/external/event_action_exporter.phpnu[. /** * Contains event class for displaying a calendar event's action. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @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 core_calendar\local\event\entities\action_interface; use core_calendar\local\event\container; use renderer_base; /** * Class for displaying a calendar event's action. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_action_exporter extends exporter { /** * Constructor. * * @param action_interface $action The action object. * @param array $related Related data. */ public function __construct(action_interface $action, $related = []) { $data = new \stdClass(); $data->name = $action->get_name(); $data->url = $action->get_url()->out(false); $data->itemcount = $action->get_item_count(); $data->actionable = $action->is_actionable(); parent::__construct($data, $related); } /** * Return the list of properties. * * @return array */ protected static function define_properties() { return [ 'name' => ['type' => PARAM_TEXT], 'url' => ['type' => PARAM_URL], 'itemcount' => ['type' => PARAM_INT], 'actionable' => ['type' => PARAM_BOOL] ]; } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'showitemcount' => ['type' => PARAM_BOOL, 'default' => false] ]; } /** * 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) { $event = $this->related['event']; if (!$event->get_component()) { return ['showitemcount' => false]; } $showitemcountcallback = 'core_calendar_event_action_shows_item_count'; $mapper = container::get_event_mapper(); $calevent = $mapper->from_event_to_legacy_event($event); $params = [$calevent, $this->data->itemcount]; $showitemcount = component_callback($event->get_component(), $showitemcountcallback, $params, false); // Prepare other values data. $data = [ 'showitemcount' => $showitemcount ]; return $data; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'context' => 'context', 'event' => '\\core_calendar\\local\\event\\entities\\event_interface' ]; } /** * Magic method returning parameters for formatting 'name' property * * @return bool[] */ protected function get_format_parameters_for_name() { return ['escape' => false]; } } PK*\ٽ5,classes/external/footer_options_exporter.phpnu[. namespace core_calendar\external; use core\external\exporter; use renderer_base; use stdClass; use moodle_url; /** * Class for exporting calendar footer view options data. * * @package core_calendar * @copyright 2017 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class footer_options_exporter extends exporter { /** * @var \calendar_information $calendar The calendar to be rendered. */ protected $calendar; /** * @var int $userid The user id. */ protected $userid; /** * @var string $token The user sha1 token. */ protected $token; /** * @var bool $showfullcalendarlink Whether the full calendar link should be displayed or not. */ protected $showfullcalendarlink; /** * Constructor for month_exporter. * * @param \calendar_information $calendar The calendar being represented * @param int $userid The user id * @param string $token The user sha1 token. * @param array $options Display options for the footer. If an option is not set, a default value will be provided. * It consists of: * - showfullcalendarlink - bool - Whether to show the full calendar link or not. Defaults to false. */ public function __construct(\calendar_information $calendar, $userid, $token, array $options = []) { $this->calendar = $calendar; $this->userid = $userid; $this->token = $token; $this->showfullcalendarlink = $options['showfullcalendarlink'] ?? false; } /** * Get manage subscription link. * * @return string|null The manage subscription hyperlink. */ protected function get_manage_subscriptions_link(): ?string { if (calendar_user_can_add_event($this->calendar->course)) { $managesubscriptionurl = new moodle_url('/calendar/managesubscriptions.php', ['course' => $this->calendar->courseid]); return $managesubscriptionurl->out(true); } return null; } /** * 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) { global $CFG; $values = new stdClass(); $values->footerlinks = []; if ($this->showfullcalendarlink) { if ($this->calendar->courseid !== SITEID) { $linkname = get_string('coursecalendarlink', 'calendar'); } else { $linkname = get_string('fullcalendar', 'calendar'); } $values->footerlinks[] = (object)[ 'url' => $this->get_calendar_url(), 'linkname' => $linkname, ]; } if (!empty($CFG->enablecalendarexport) && $managesubscriptionlink = $this->get_manage_subscriptions_link()) { $values->footerlinks[] = (object)[ 'url' => $managesubscriptionlink, 'linkname' => get_string('managesubscriptions', 'calendar'), ]; } return (array) $values; } /** * Return the list of additional properties. * * @return array */ public static function define_other_properties() { return [ 'footerlinks' => [ 'type' => [ 'url' => [ 'type' => PARAM_URL, ], 'linkname' => [ 'type' => PARAM_TEXT, ], ], 'multiple' => true, 'optional' => true, ], ]; } /** * Build the calendar URL. * * @return string The calendar URL. */ public function get_calendar_url() { $url = new moodle_url('/calendar/view.php', [ 'view' => 'month', 'time' => $this->calendar->time, 'course' => $this->calendar->courseid, ]); return $url->out(false); } } PK*\^_ _ &classes/external/day_name_exporter.phpnu[. /** * Contains event class for displaying the day name. * * @package core_calendar * @copyright 2017 Andrew Nicols * @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; /** * Class for displaying the day view. * * @package core_calendar * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class day_name_exporter extends exporter { /** * @var int $dayno The day number. */ protected $dayno; /** * @var string $shortname The formatted short name of the day. */ protected $shortname; /** * @var string $fullname The formatted full name of the day. */ protected $fullname; /** * Constructor. * * @param int $dayno The day number. * @param array $names The list of names. */ public function __construct($dayno, $names) { $data = $names + ['dayno' => $dayno]; parent::__construct($data, []); } /** * Return the list of properties. * * @return array */ protected static function define_properties() { return [ 'dayno' => [ 'type' => PARAM_INT, ], 'shortname' => [ // Note: The calendar type class has already formatted the names. 'type' => PARAM_RAW, ], 'fullname' => [ // Note: The calendar type class has already formatted the names. 'type' => PARAM_RAW, ], ]; } } PK*\"'͛yyclasses/privacy/provider.phpnu[. /** * Privacy class for requesting user data. * * @package core_calendar * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\context; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\transform; use \core_privacy\local\request\writer; use \core_privacy\local\request\userlist; use \core_privacy\local\request\approved_userlist; /** * Privacy Subsystem for core_calendar implementing metadata, plugin, and user_preference providers. * * @package core_calendar * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\user_preference_provider { /** * Provides meta data that is stored about a user with core_calendar. * * @param collection $collection A collection of meta data items to be added to. * @return collection Returns the collection of metadata. */ public static function get_metadata(collection $collection): collection { // The calendar 'event' table contains user data. $collection->add_database_table( 'event', [ 'name' => 'privacy:metadata:calendar:event:name', 'description' => 'privacy:metadata:calendar:event:description', 'eventtype' => 'privacy:metadata:calendar:event:eventtype', 'timestart' => 'privacy:metadata:calendar:event:timestart', 'timeduration' => 'privacy:metadata:calendar:event:timeduration', ], 'privacy:metadata:calendar:event' ); // The calendar 'event_subscriptions' table contains user data. $collection->add_database_table( 'event_subscriptions', [ 'name' => 'privacy:metadata:calendar:event_subscriptions:name', 'url' => 'privacy:metadata:calendar:event_subscriptions:url', 'eventtype' => 'privacy:metadata:calendar:event_subscriptions:eventtype', ], 'privacy:metadata:calendar:event_subscriptions' ); // The calendar user preference setting 'calendar_savedflt'. $collection->add_user_preference( 'calendar_savedflt', 'privacy:metadata:calendar:preferences:calendar_savedflt' ); return $collection; } /** * Get the list of contexts that contain calendar user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { $contextlist = new contextlist(); // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts. $params = [ 'sitecontext' => CONTEXT_SYSTEM, 'categorycontext' => CONTEXT_COURSECAT, 'coursecontext' => CONTEXT_COURSE, 'groupcontext' => CONTEXT_COURSE, 'usercontext' => CONTEXT_USER, 'cuserid' => $userid, 'modulecontext' => CONTEXT_MODULE, 'muserid' => $userid ]; // Get contexts of Calendar Events for the owner. $sql = "SELECT ctx.id FROM {context} ctx JOIN {event} e ON (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR (e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR (e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext) WHERE e.userid = :cuserid"; $contextlist->add_from_sql($sql, $params); $sql = "SELECT ctx.id FROM {context} ctx JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext JOIN {modules} m ON m.id = cm.module JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance WHERE e.userid = :muserid"; $contextlist->add_from_sql($sql, $params); // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts. $params = [ 'sitecontext' => CONTEXT_SYSTEM, 'categorycontext' => CONTEXT_COURSECAT, 'coursecontext' => CONTEXT_COURSE, 'groupcontext' => CONTEXT_COURSE, 'usercontext' => CONTEXT_USER, 'userid' => $userid ]; // Get contexts for Calendar Subscriptions for the owner. $sql = "SELECT ctx.id FROM {context} ctx JOIN {event_subscriptions} s ON (s.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR (s.categoryid = ctx.instanceid AND s.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR (s.courseid = ctx.instanceid AND s.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR (s.courseid = ctx.instanceid AND s.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR (s.userid = ctx.instanceid AND s.eventtype = 'user' AND ctx.contextlevel = :usercontext) WHERE s.userid = :userid"; $contextlist->add_from_sql($sql, $params); // Return combined contextlist for Calendar Events & Calendar Subscriptions. return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { global $DB; $context = $userlist->get_context(); // Calendar Events can exist at Site (CONTEXT_SYSTEM), Course Category (CONTEXT_COURSECAT), // Course and Course Group (CONTEXT_COURSE), User (CONTEXT_USER), or Course Modules (CONTEXT_MODULE) contexts. if ($context->contextlevel == CONTEXT_MODULE) { $params = ['cmid' => $context->instanceid]; $sql = "SELECT e.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance WHERE cm.id = :cmid"; $userlist->add_from_sql('userid', $sql, $params); } else if ($context->contextlevel == CONTEXT_SYSTEM) { // Get contexts of Calendar Events for the owner. $sql = "SELECT userid FROM {event} WHERE eventtype = 'site'"; $userlist->add_from_sql('userid', $sql, []); // Get contexts for Calendar Subscriptions for the owner. $sql = "SELECT userid FROM {event_subscriptions} WHERE eventtype = 'site'"; $userlist->add_from_sql('userid', $sql, []); } else if (in_array($context->contextlevel, [CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_USER])) { $eventfields = [ CONTEXT_COURSECAT => 'categoryid', CONTEXT_COURSE => 'courseid', CONTEXT_USER => 'userid' ]; $eventfield = $eventfields[$context->contextlevel]; $eventtypes = [ CONTEXT_COURSECAT => 'category', CONTEXT_COURSE => ['course' , 'group'], CONTEXT_USER => 'user' ]; list($eventtypesql, $eventtypeparams) = $DB->get_in_or_equal($eventtypes[$context->contextlevel], SQL_PARAMS_NAMED); $params = $eventtypeparams + ['instanceid' => $context->instanceid]; // Get contexts of Calendar Events for the owner. $sql = "SELECT userid FROM {event} WHERE eventtype $eventtypesql AND $eventfield = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Get contexts for Calendar Subscriptions for the owner. $sql = "SELECT userid FROM {event_subscriptions} WHERE eventtype $eventtypesql AND $eventfield = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); } } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { if (empty($contextlist)) { return; } self::export_user_calendar_event_data($contextlist); self::export_user_calendar_subscription_data($contextlist); } /** * Export all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { $calendarsavedflt = get_user_preferences('calendar_savedflt', null, $userid); if (null !== $calendarsavedflt) { writer::export_user_preference( 'core_calendar', 'calendarsavedflt', $calendarsavedflt, get_string('privacy:metadata:calendar:preferences:calendar_savedflt', 'core_calendar') ); } } /** * Delete all Calendar Event and Calendar Subscription data for all users in the specified context. * * @param \context $context Transform the specific context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { // Delete all Calendar Events in the specified context in batches. if ($eventids = array_keys(self::get_calendar_event_ids_by_context($context))) { self::delete_batch_records('event', 'id', $eventids); } // Delete all Calendar Subscriptions in the specified context in batches. if ($subscriptionids = array_keys(self::get_calendar_subscription_ids_by_context($context))) { self::delete_batch_records('event_subscriptions', 'id', $subscriptionids); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); $userids = $userlist->get_userids(); $allowedcontexts = [ CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_MODULE, CONTEXT_USER ]; if (!in_array($context->contextlevel, $allowedcontexts)) { return; } if (empty($userids)) { return; } // Delete all Calendar Events in the specified context in batches. if ($eventids = array_keys(self::get_calendar_event_ids_by_context($context, $userids))) { self::delete_batch_records('event', 'id', $eventids); } // Delete all Calendar Subscriptions in the specified context in batches. if ($subscriptionids = array_keys(self::get_calendar_subscription_ids_by_context($context, $userids))) { self::delete_batch_records('event_subscriptions', 'id', $subscriptionids); } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { if (empty($contextlist)) { return; } // Delete all Calendar Events for the owner and specified contexts in batches. $eventdetails = self::get_calendar_event_details_by_contextlist($contextlist); $eventids = []; foreach ($eventdetails as $eventdetail) { $eventids[] = $eventdetail->eventid; } $eventdetails->close(); self::delete_batch_records('event', 'id', $eventids); // Delete all Calendar Subscriptions for the owner and specified contexts in batches. $subscriptiondetails = self::get_calendar_subscription_details_by_contextlist($contextlist); $subscriptionids = []; foreach ($subscriptiondetails as $subscriptiondetail) { $subscriptionids[] = $subscriptiondetail->subscriptionid; } $subscriptiondetails->close(); self::delete_batch_records('event_subscriptions', 'id', $subscriptionids); } /** * Helper function to export Calendar Events data by a User's contextlist. * * @param approved_contextlist $contextlist * @throws \coding_exception */ protected static function export_user_calendar_event_data(approved_contextlist $contextlist) { // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts. $eventdetails = self::get_calendar_event_details_by_contextlist($contextlist); // Multiple Calendar Events of the same eventtype and time can exist for a context, so collate them for export. $eventrecords = []; foreach ($eventdetails as $eventdetail) { // Create an array key based on the contextid, eventtype, and time. $key = $eventdetail->contextid . $eventdetail->eventtype . $eventdetail->timestart; if (array_key_exists($key, $eventrecords) === false) { $eventrecords[$key] = [ $eventdetail ]; } else { $eventrecords[$key] = array_merge($eventrecords[$key], [$eventdetail]); } } $eventdetails->close(); // Export Calendar Event data. foreach ($eventrecords as $eventrecord) { $index = (count($eventrecord) > 1) ? 1 : 0; foreach ($eventrecord as $event) { // Export the events using the structure Calendar/Events/{datetime}/{eventtype}-event.json. $subcontexts = [ get_string('calendar', 'calendar'), get_string('events', 'calendar'), date('c', $event->timestart) ]; $name = $event->eventtype . '-event'; // Use name {eventtype}-event-{index}.json if multiple eventtypes and time exists at the same context. if ($index != 0) { $name .= '-' . $index; $index++; } $eventdetails = (object) [ 'name' => $event->name, 'description' => $event->description, 'location' => $event->location, 'eventtype' => $event->eventtype, 'timestart' => transform::datetime($event->timestart), 'timeduration' => $event->timeduration ]; $context = \context::instance_by_id($event->contextid); writer::with_context($context)->export_related_data($subcontexts, $name, $eventdetails); } } } /** * Helper function to export Calendar Subscriptions data by a User's contextlist. * * @param approved_contextlist $contextlist * @throws \coding_exception */ protected static function export_user_calendar_subscription_data(approved_contextlist $contextlist) { // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts. $subscriptiondetails = self::get_calendar_subscription_details_by_contextlist($contextlist); // Multiple Calendar Subscriptions of the same eventtype can exist for a context, so collate them for export. $subscriptionrecords = []; foreach ($subscriptiondetails as $subscriptiondetail) { // Create an array key based on the contextid and eventtype. $key = $subscriptiondetail->contextid . $subscriptiondetail->eventtype; if (array_key_exists($key, $subscriptionrecords) === false) { $subscriptionrecords[$key] = [ $subscriptiondetail ]; } else { $subscriptionrecords[$key] = array_merge($subscriptionrecords[$key], [$subscriptiondetail]); } } $subscriptiondetails->close(); // Export Calendar Subscription data. foreach ($subscriptionrecords as $subscriptionrecord) { $index = (count($subscriptionrecord) > 1) ? 1 : 0; foreach ($subscriptionrecord as $subscription) { // Export the events using the structure Calendar/Subscriptions/{eventtype}-subscription.json. $subcontexts = [ get_string('calendar', 'calendar'), get_string('subscriptions', 'calendar') ]; $name = $subscription->eventtype . '-subscription'; // Use name {eventtype}-subscription-{index}.json if multiple eventtypes exists at the same context. if ($index != 0) { $name .= '-' . $index; $index++; } $context = \context::instance_by_id($subscription->contextid); writer::with_context($context)->export_related_data($subcontexts, $name, $subscription); } } } /** * Helper function to return all Calendar Event id results for a specified context and optionally * included user list. * * @param \context $context * @param array $userids * @return array|null * @throws \dml_exception */ protected static function get_calendar_event_ids_by_context(\context $context, $userids = array()) { global $DB; // Calendar Events can exist at Site (CONTEXT_SYSTEM), Course Category (CONTEXT_COURSECAT), // Course and Course Group (CONTEXT_COURSE), User (CONTEXT_USER), or Course Modules (CONTEXT_MODULE) contexts. if (!in_array($context->contextlevel, [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_USER, CONTEXT_MODULE])) { return []; } $whereusersql = ''; $userparams = array(); if (!empty($userids)) { list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); $whereusersql = "AND e.userid {$usersql}"; } if ($context->contextlevel == CONTEXT_MODULE) { // Course Module events. $params = ['cmid' => $context->instanceid]; // Get Calendar Events for the specified Course Module context. $sql = "SELECT DISTINCT e.id AS eventid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance WHERE cm.id = :cmid $whereusersql"; } else if ($context->contextlevel == CONTEXT_SYSTEM) { // Site events. $params = []; $sql = "SELECT DISTINCT e.id AS eventid FROM {event} e WHERE e.eventtype = 'site' $whereusersql"; } else { // The rest. $eventfields = [ CONTEXT_COURSECAT => 'categoryid', CONTEXT_COURSE => 'courseid', CONTEXT_USER => 'userid' ]; $eventfield = $eventfields[$context->contextlevel]; $eventtypes = [ CONTEXT_COURSECAT => 'category', CONTEXT_COURSE => ['course' , 'group'], CONTEXT_USER => 'user' ]; list($eventtypesql, $eventtypeparams) = $DB->get_in_or_equal($eventtypes[$context->contextlevel], SQL_PARAMS_NAMED); $params = $eventtypeparams + ['instanceid' => $context->instanceid]; // Get Calendar Events for the specified Moodle context. $sql = "SELECT DISTINCT e.id AS eventid FROM {event} e WHERE e.eventtype $eventtypesql AND e.{$eventfield} = :instanceid $whereusersql"; } $params += $userparams; return $DB->get_records_sql($sql, $params); } /** * Helper function to return all Calendar Subscription id results for a specified context and optionally * included user list. * * @param \context $context * @param array $userids * @return array * @throws \dml_exception */ protected static function get_calendar_subscription_ids_by_context(\context $context, $userids = array()) { global $DB; // Calendar Subscriptions can exist at Site (CONTEXT_SYSTEM), Course Category (CONTEXT_COURSECAT), // Course and Course Group (CONTEXT_COURSE), or User (CONTEXT_USER) contexts. if (!in_array($context->contextlevel, [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_USER])) { return []; } $whereusersql = ''; $userparams = array(); if (!empty($userids)) { list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); $whereusersql = "AND s.userid {$usersql}"; } if ($context->contextlevel == CONTEXT_SYSTEM) { $params = []; // Get Calendar Subscriptions for the system context. $sql = "SELECT DISTINCT s.id AS subscriptionid FROM {event_subscriptions} s WHERE s.eventtype = 'site' $whereusersql"; } else { $eventfields = [ CONTEXT_COURSECAT => 'categoryid', CONTEXT_COURSE => 'courseid', CONTEXT_USER => 'userid' ]; $eventfield = $eventfields[$context->contextlevel]; $eventtypes = [ CONTEXT_COURSECAT => 'category', CONTEXT_COURSE => ['course' , 'group'], CONTEXT_USER => 'user' ]; list($eventtypesql, $eventtypeparams) = $DB->get_in_or_equal($eventtypes[$context->contextlevel], SQL_PARAMS_NAMED); $params = $eventtypeparams + ['instanceid' => $context->instanceid]; // Get Calendar Subscriptions for the specified context. $sql = "SELECT DISTINCT s.id AS subscriptionid FROM {event_subscriptions} s WHERE s.eventtype $eventtypesql AND s.{$eventfield} = :instanceid $whereusersql"; } $params += $userparams; return $DB->get_records_sql($sql, $params); } /** * Helper function to return the Calendar Events for a given user and context list. * * @param approved_contextlist $contextlist * @return \moodle_recordset * @throws \coding_exception * @throws \dml_exception */ protected static function get_calendar_event_details_by_contextlist(approved_contextlist $contextlist) { global $DB; $userid = $contextlist->get_user()->id; list($contextsql1, $contextparams1) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); list($contextsql2, $contextparams2) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts. $params = [ 'sitecontext' => CONTEXT_SYSTEM, 'categorycontext' => CONTEXT_COURSECAT, 'coursecontext' => CONTEXT_COURSE, 'groupcontext' => CONTEXT_COURSE, 'usercontext' => CONTEXT_USER, 'cuserid' => $userid, 'modulecontext' => CONTEXT_MODULE, 'muserid' => $userid ]; $params += $contextparams1; $params += $contextparams2; // Get Calendar Events details for the approved contexts and the owner. $sql = "SELECT ctxid as contextid, details.id as eventid, details.name as name, details.description as description, details.location as location, details.eventtype as eventtype, details.timestart as timestart, details.timeduration as timeduration FROM ( SELECT e.id AS id, ctx.id AS ctxid FROM {context} ctx INNER JOIN {event} e ON (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR (e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR (e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext) WHERE e.userid = :cuserid AND ctx.id {$contextsql1} UNION SELECT e.id AS id, ctx.id AS ctxid FROM {context} ctx INNER JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext INNER JOIN {modules} m ON m.id = cm.module INNER JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance WHERE e.userid = :muserid AND ctx.id {$contextsql2} ) ids JOIN {event} details ON details.id = ids.id ORDER BY ids.id"; return $DB->get_recordset_sql($sql, $params); } /** * Helper function to return the Calendar Subscriptions for a given user and context list. * * @param approved_contextlist $contextlist * @return \moodle_recordset * @throws \coding_exception * @throws \dml_exception */ protected static function get_calendar_subscription_details_by_contextlist(approved_contextlist $contextlist) { global $DB; $user = $contextlist->get_user(); list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); $params = [ 'sitecontext' => CONTEXT_SYSTEM, 'categorycontext' => CONTEXT_COURSECAT, 'coursecontext' => CONTEXT_COURSE, 'groupcontext' => CONTEXT_COURSE, 'usercontext' => CONTEXT_USER, 'userid' => $user->id ]; $params += $contextparams; // Get Calendar Subscriptions for the approved contexts and the owner. $sql = "SELECT DISTINCT c.id as contextid, s.id as subscriptionid, s.name as name, s.url as url, s.eventtype as eventtype FROM {context} c INNER JOIN {event_subscriptions} s ON (s.eventtype = 'site' AND c.contextlevel = :sitecontext) OR (s.categoryid = c.instanceid AND s.eventtype = 'category' AND c.contextlevel = :categorycontext) OR (s.courseid = c.instanceid AND s.eventtype = 'course' AND c.contextlevel = :coursecontext) OR (s.courseid = c.instanceid AND s.eventtype = 'group' AND c.contextlevel = :groupcontext) OR (s.userid = c.instanceid AND s.eventtype = 'user' AND c.contextlevel = :usercontext) WHERE s.userid = :userid AND c.id {$contextsql}"; return $DB->get_recordset_sql($sql, $params); } /** * Helper function to delete records in batches in order to minimise amount of deletion queries. * * @param string $tablename The table name to delete from. * @param string $field The table column field name to delete records by. * @param array $values The table column field values to delete records by. * @throws \dml_exception */ protected static function delete_batch_records($tablename, $field, $values) { global $DB; // Batch deletion with an upper limit of 2000 records to minimise the number of deletion queries. $batchrecords = array_chunk($values, 2000); foreach ($batchrecords as $batchrecord) { $DB->delete_records_list($tablename, $field, $batchrecord); } } } PK*\< classes/type_factory.phpnu[. namespace core_calendar; /** * Class \core_calendar\type_factory. * * Factory class producing required subclasses of {@link \core_calendar\type_base}. * * @package core_calendar * @copyright 2008 onwards Foodle Group {@link http://foodle.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class type_factory { /** * Returns an instance of the currently used calendar type. * * @param string|null $type the calendar type to use, if none provided use logic to determine * @return \core_calendar\type_base the created calendar_type class * @throws \coding_exception if the calendar type file could not be loaded */ public static function get_calendar_instance($type = null) { if (is_null($type)) { $type = self::get_calendar_type(); } $class = "\\calendartype_$type\\structure"; // Ensure the calendar type exists. It may occur that a user has selected a calendar type, which was then // deleted. If this happens we want to fall back on the Gregorian calendar type. if (!class_exists($class)) { $class = "\\calendartype_gregorian\\structure"; } return new $class(); } /** * Returns a list of calendar typess available for use. * * @return array the list of calendar types */ public static function get_list_of_calendar_types() { $calendars = array(); $calendardirs = \core_component::get_plugin_list('calendartype'); foreach ($calendardirs as $name => $location) { $calendars[$name] = get_string('name', "calendartype_{$name}"); } return $calendars; } /** * Returns the current calendar type in use. * * @return string the current calendar type being used */ public static function get_calendar_type() { global $CFG, $USER, $SESSION, $COURSE; // Course calendartype can override all other settings for this page. if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->calendartype)) { $return = $COURSE->calendartype; } else if (!empty($SESSION->calendartype)) { // Session calendartype can override other settings. $return = $SESSION->calendartype; } else if (!empty($USER->calendartype)) { $return = $USER->calendartype; } else if (!empty($CFG->calendartype)) { $return = $CFG->calendartype; } else { $return = 'gregorian'; } return $return; } } PK*\Ҁ t5t5classes/local/api.phpnu[. /** * Contains class containing the internal calendar API. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local; defined('MOODLE_INTERNAL') || die(); use core_calendar\local\event\container; use core_calendar\local\event\entities\event_interface; use core_calendar\local\event\exceptions\limit_invalid_parameter_exception; /** * Class containing the local calendar API. * * This should not be used outside of core_calendar. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { /** * Get all events restricted by various parameters, taking in to account user and group overrides. * * @param int|null $timestartfrom Events with timestart from this value (inclusive). * @param int|null $timestartto Events with timestart until this value (inclusive). * @param int|null $timesortfrom Events with timesort from this value (inclusive). * @param int|null $timesortto Events with timesort until this value (inclusive). * @param int|null $timestartaftereventid Restrict the events in the timestart range to ones after this ID. * @param int|null $timesortaftereventid Restrict the events in the timesort range to ones after this ID. * @param int $limitnum Return at most this number of events. * @param int|null $type Return only events of this type. * @param array|null $usersfilter Return only events for these users. * @param array|null $groupsfilter Return only events for these groups. * @param array|null $coursesfilter Return only events for these courses. * @param bool $withduration If true return only events starting within specified * timestart otherwise return in progress events as well. * @param bool $ignorehidden If true don't return hidden events. * @return \core_calendar\local\event\entities\event_interface[] Array of event_interfaces. */ public static function get_events( $timestartfrom = null, $timestartto = null, $timesortfrom = null, $timesortto = null, $timestartaftereventid = null, $timesortaftereventid = null, $limitnum = 20, $type = null, ?array $usersfilter = null, ?array $groupsfilter = null, ?array $coursesfilter = null, ?array $categoriesfilter = null, $withduration = true, $ignorehidden = true, ?callable $filter = null ) { global $USER; $vault = \core_calendar\local\event\container::get_event_vault(); $timestartafterevent = null; $timesortafterevent = null; if ($timestartaftereventid && $event = $vault->get_event_by_id($timestartaftereventid)) { $timestartafterevent = $event; } if ($timesortaftereventid && $event = $vault->get_event_by_id($timesortaftereventid)) { $timesortafterevent = $event; } return $vault->get_events( $timestartfrom, $timestartto, $timesortfrom, $timesortto, $timestartafterevent, $timesortafterevent, $limitnum, $type, $usersfilter, $groupsfilter, $coursesfilter, $categoriesfilter, $withduration, $ignorehidden, $filter ); } /** * Get a list of action events for the logged in user by the given * timesort values. * * @param int|null $timesortfrom The start timesort value (inclusive) * @param int|null $timesortto The end timesort value (inclusive) * @param int|null $aftereventid Only return events after this one * @param int $limitnum Limit results to this amount (between 1 and 50) * @param bool $lmittononsuspendedevents Limit course events to courses the user is active in (not suspended). * @param \stdClass|null $user The user id or false for $USER * @param string|null $searchvalue The value a user wishes to search against * @return array A list of action_event_interface objects * @throws \moodle_exception */ public static function get_action_events_by_timesort( $timesortfrom = null, $timesortto = null, $aftereventid = null, $limitnum = 20, $limittononsuspendedevents = false, ?\stdClass $user = null, ?string $searchvalue = null ) { global $USER; if (!$user) { $user = $USER; } if (is_null($timesortfrom) && is_null($timesortto)) { throw new \moodle_exception("Must provide a timesort to and/or from value"); } if ($limitnum < 1 || $limitnum > 50) { throw new \moodle_exception("Limit must be between 1 and 50 (inclusive)"); } \core_calendar\local\event\container::set_requesting_user($user->id); $vault = \core_calendar\local\event\container::get_event_vault(); $afterevent = null; if ($aftereventid && $event = $vault->get_event_by_id($aftereventid)) { $afterevent = $event; } return $vault->get_action_events_by_timesort($user, $timesortfrom, $timesortto, $afterevent, $limitnum, $limittononsuspendedevents, $searchvalue); } /** * Get a list of action events for the logged in user by the given * course and timesort values. * * @param \stdClass $course The course the events must belong to * @param int|null $timesortfrom The start timesort value (inclusive) * @param int|null $timesortto The end timesort value (inclusive) * @param int|null $aftereventid Only return events after this one * @param int $limitnum Limit results to this amount (between 1 and 50) * @param string|null $searchvalue The value a user wishes to search against * @return array A list of action_event_interface objects * @throws limit_invalid_parameter_exception */ public static function get_action_events_by_course( $course, $timesortfrom = null, $timesortto = null, $aftereventid = null, $limitnum = 20, ?string $searchvalue = null ) { global $USER; if ($limitnum < 1 || $limitnum > 50) { throw new limit_invalid_parameter_exception( "Limit must be between 1 and 50 (inclusive)"); } $vault = \core_calendar\local\event\container::get_event_vault(); $afterevent = null; if ($aftereventid && $event = $vault->get_event_by_id($aftereventid)) { $afterevent = $event; } return $vault->get_action_events_by_course( $USER, $course, $timesortfrom, $timesortto, $afterevent, $limitnum, $searchvalue); } /** * Get a list of action events for the logged in user by the given * courses and timesort values. * * The limit number applies per course, not for the result set as a whole. * E.g. Requesting 3 courses with a limit of 10 will result in up to 30 * events being returned (up to 10 per course). * * @param array $courses The courses the events must belong to * @param int|null $timesortfrom The start timesort value (inclusive) * @param int|null $timesortto The end timesort value (inclusive) * @param int $limitnum Limit results per course to this amount (between 1 and 50) * @param string|null $searchvalue The value a user wishes to search against * @return array A list of action_event_interface objects indexed by course id */ public static function get_action_events_by_courses( $courses = [], $timesortfrom = null, $timesortto = null, $limitnum = 20, ?string $searchvalue = null ) { $return = []; foreach ($courses as $course) { $return[$course->id] = self::get_action_events_by_course( $course, $timesortfrom, $timesortto, null, $limitnum, $searchvalue ); } return $return; } /** * Change the start day for an event. Only the date will be * modified, the time of day for the event will be left as is. * * @param event_interface $event The existing event to modify * @param DateTimeInterface $startdate The new date to use for the start day * @return event_interface The new event with updated start date */ public static function update_event_start_day( event_interface $event, \DateTimeInterface $startdate ) { global $DB; $mapper = container::get_event_mapper(); $legacyevent = $mapper->from_event_to_legacy_event($event); $hascoursemodule = !empty($event->get_course_module()); $moduleinstance = null; $starttime = $event->get_times()->get_start_time()->setDate( $startdate->format('Y'), $startdate->format('n'), $startdate->format('j') ); $starttimestamp = $starttime->getTimestamp(); if ($hascoursemodule) { $moduleinstance = $DB->get_record( $event->get_course_module()->get('modname'), ['id' => $event->get_course_module()->get('instance')], '*', MUST_EXIST ); // If there is a timestart range callback implemented then we can // use the values returned from the valid timestart range to apply // some default validation on the event's timestart value to ensure // that it falls within the specified range. list($min, $max) = component_callback( 'mod_' . $event->get_course_module()->get('modname'), 'core_calendar_get_valid_event_timestart_range', [$legacyevent, $moduleinstance], [false, false] ); } else if ($legacyevent->courseid != 0 && $legacyevent->courseid != SITEID && $legacyevent->groupid == 0) { // This is a course event. list($min, $max) = component_callback( 'core_course', 'core_calendar_get_valid_event_timestart_range', [$legacyevent, $event->get_course()->get_proxied_instance()], [0, 0] ); } else { $min = $max = 0; } // If the callback returns false for either value it means that // there is no valid time start range. if ($min === false || $max === false) { throw new \moodle_exception('The start day of this event can not be modified'); } if ($min && $starttimestamp < $min[0]) { throw new \moodle_exception($min[1]); } if ($max && $starttimestamp > $max[0]) { throw new \moodle_exception($max[1]); } // This function does our capability checks. $legacyevent->update((object) ['timestart' => $starttime->getTimestamp()]); // Check that the user is allowed to manually edit calendar events before // calling the event updated callback. The manual flag causes the code to // check the user has the capabilities to modify the modules. // // We don't want to call the event update callback if the user isn't allowed // to modify course modules because depending on the callback it can make // some changes that would be considered security issues, such as updating the // due date for an assignment. if ($hascoursemodule && calendar_edit_event_allowed($legacyevent, true)) { // If this event is from an activity then we need to call // the activity callback to let it know that the event it // created has been modified so it needs to update accordingly. component_callback( 'mod_' . $event->get_course_module()->get('modname'), 'core_calendar_event_timestart_updated', [$legacyevent, $moduleinstance] ); // Rebuild the course cache to make sure the updated dates are reflected. $courseid = $event->get_course()->get('id'); $cmid = $event->get_course_module()->get('id'); \course_modinfo::purge_course_module_cache($courseid, $cmid); rebuild_course_cache($courseid, true, true); } return $mapper->from_legacy_event_to_event($legacyevent); } } PK*\Ë  Iclasses/local/event/strategies/raw_event_retrieval_strategy_interface.phpnu[. /** * Raw event strategy retrieval interface. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\strategies; defined('MOODLE_INTERNAL') || die(); /** * Interface for an raw event retrival strategy class. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface raw_event_retrieval_strategy_interface { /** * Retrieve raw calendar event records from the DB. * * @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. * @param int|null $offset Amount to offset results by. * @param int $limitnum Return at most this many results. * @param bool $ignorehidden True to ignore hidden events. False to include them. * @return \stdClass[] Array of event records. */ public function get_raw_events( ?array $usersfilter = null, ?array $groupsfilter = null, ?array $coursesfilter = null, ?array $categoriesfilter = null, ?array $whereconditions = null, ?array $whereparams = null, $ordersql = null, $offset = null, $limitnum = 40, $ignorehidden = true ); } PK*\[+PWDWD?classes/local/event/strategies/raw_event_retrieval_strategy.phpnu[. /** * Raw event retrieval strategy. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\strategies; defined('MOODLE_INTERNAL') || die(); /** * Raw event retrieval strategy. * * This strategy is based on what used to be the calendar API's get_events function. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_interface { public function get_raw_events( ?array $usersfilter = null, ?array $groupsfilter = null, ?array $coursesfilter = null, ?array $categoriesfilter = null, ?array $whereconditions = null, ?array $whereparams = null, $ordersql = null, $offset = null, $limitnum = null, $ignorehidden = true ) { return $this->get_raw_events_legacy_implementation( !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, $offset, $limitnum, $ignorehidden ); } /** * The legacy implementation with minor tweaks. * * @param array|int|boolean $users array of users, user id or boolean for all/no user events * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events * @param array $whereconditions The conditions in the WHERE clause. * @param array $whereparams The parameters for the WHERE clause. * @param string $ordersql The ORDER BY clause. * @param int $offset Offset. * @param int $limitnum Limit. * @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) */ protected function get_raw_events_legacy_implementation( $users, $groups, $courses, $categories, $whereconditions, $whereparams, $ordersql, $offset, $limitnum, $ignorehidden ) { global $DB; $params = array(); // Quick test. if (empty($users) && empty($groups) && empty($courses) && empty($categories)) { return array(); } if (is_numeric($users)) { $users = array($users); } if (is_numeric($groups)) { $groups = array($groups); } if (is_numeric($courses)) { $courses = array($courses); } if (is_numeric($categories)) { $categories = array($categories); } // Array of filter conditions. To be concatenated by the OR operator. $filters = []; // User filter. if (is_array($users) && !empty($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 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 AND e.categoryid = 0)"; } // Boolean false (no users at all): We don't need to do anything. // Group filter. if (is_array($groups) && !empty($groups)) { // Events from a number of groups. list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); $filters[] = "e.groupid $insqlgroups"; $params = array_merge($params, $inparamsgroups); } else if ($groups === true) { // Events from ALL groups. $filters[] = "e.groupid != 0"; } // Boolean false (no groups at all): We don't need to do anything. // Course filter. if (is_array($courses) && !empty($courses)) { list($insqlcourses, $inparamscourses) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED); $filters[] = "(e.groupid = 0 AND e.courseid $insqlcourses)"; $params = array_merge($params, $inparamscourses); } else if ($courses === true) { // Events from ALL courses. $filters[] = "(e.groupid = 0 AND e.courseid != 0)"; } // Category filter. if (is_array($categories) && !empty($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 // valid query with only time constraints, thus selecting ALL events in that time frame! if (empty($filters)) { return array(); } // Build our clause for the filters. $filterclause = implode(' OR ', $filters); // Array of where conditions for our query. To be concatenated by the AND operator. $whereconditions[] = "($filterclause)"; // Show visible only. if ($ignorehidden) { $whereconditions[] = "(e.visible = 1)"; } // Build the main query's WHERE clause. $whereclause = implode(' AND ', $whereconditions); // Build SQL subquery and conditions for filtered events based on priorities. $subquerytimeconditions = array_filter($whereconditions, function($condition) { return (strpos($condition, 'time') !== false); }); $subquerywhere = ''; $subqueryconditions = []; $subqueryparams = []; $allusercourses = []; $groupcourses = []; if (is_array($groups) && !empty($groups)) { $groupcourses = $DB->get_fieldset_sql( "SELECT DISTINCT courseid FROM {groups} WHERE id $insqlgroups", $inparamsgroups, ); } if (is_array($users) && !empty($users)) { $userrecords = $DB->get_records_sql("SELECT * FROM {user} WHERE id $insqlusers", $inparamsusers); foreach ($userrecords as $userrecord) { // Get the user's courses. Otherwise, get the default courses being shown by the calendar. $usercourses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce', false, $userrecord->id); $filteredcourses = array_merge( is_array($courses) ? $courses : [$courses], $groupcourses, // Include courses for any groups we've specified. ); $filteredcourses = array_filter($usercourses, fn($course) => in_array($course->id, $filteredcourses)); // Set calendar filters. [$filteredcourses, $usergroups, $user] = calendar_set_filters($filteredcourses, true, $userrecord); $allusercourses = array_merge($allusercourses, $filteredcourses); // Flag to indicate whether the query needs to exclude group overrides. $viewgroupsonly = false; if ($user) { // Set filter condition for the user's events. // Even though $user is a single scalar, we still use get_in_or_equal() because we are inside a loop. list($inusers, $inuserparams) = $DB->get_in_or_equal($user, SQL_PARAMS_NAMED); $condition = "(ev.userid $inusers AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)"; $subqueryconditions[] = $condition; $subqueryparams = array_merge($subqueryparams, $inuserparams); foreach ($filteredcourses as $courseid) { if (has_capability('moodle/site:accessallgroups', \context_course::instance($courseid), $userrecord)) { $usergroupmembership = groups_get_all_groups($courseid, $user, 0, 'g.id'); if (count($usergroupmembership) == 0) { $viewgroupsonly = true; break; } } } } // Set filter condition for the user's group events. if ($usergroups === true || $viewgroupsonly) { // Fetch group events, but not group overrides. $groupconditions = "(ev.groupid != 0 AND ev.eventtype = 'group')"; } else if (!empty($usergroups)) { // Fetch group events and group overrides. list($inusergroups, $inusergroupparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED); $groupconditions = "(ev.groupid $inusergroups)"; $subqueryparams = array_merge($subqueryparams, $inusergroupparams); } } } else if ($users === true) { // Events from ALL users. $subqueryconditions[] = "(ev.userid != 0 AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)"; if (is_array($groups)) { // Events from a number of groups. list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); $subqueryconditions[] = "ev.groupid $insqlgroups"; $subqueryparams = array_merge($subqueryparams, $inparamsgroups); } else if ($groups === true) { // Events from ALL groups. $subqueryconditions[] = "ev.groupid != 0"; } if ($courses === true) { // ALL course events. It's not needed to worry about users' access as $users = true. $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid != 0 AND ev.categoryid = 0)"; } } // Get courses to be used for the subquery. $subquerycourses = []; if (is_array($courses)) { $subquerycourses = $courses; } // Merge with user courses, if necessary. if (!empty($allusercourses)) { $subquerycourses = array_merge($subquerycourses, $allusercourses); // Make sure we remove duplicate values. $subquerycourses = array_unique($subquerycourses); } // Set subquery filter condition for the courses. if (!empty($subquerycourses)) { list($incourses, $incoursesparams) = $DB->get_in_or_equal($subquerycourses, SQL_PARAMS_NAMED); if (isset($groupconditions)) { $groupconditions = $groupconditions." OR "; } else { $groupconditions = ''; } $condition = "($groupconditions(ev.groupid = 0 AND ev.courseid $incourses AND ev.categoryid = 0))"; $subtimesparams = []; if (!empty($subquerytimeconditions)) { $subtimes = $this->subquerytimeconditions("courses", $subquerytimeconditions, $whereparams); $condition .= $subtimes['where']; $subtimesparams = $subtimes['params']; } $subqueryconditions[] = $condition; $subqueryparams = array_merge($subqueryparams, $incoursesparams, $subtimesparams); } // 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); $condition = "(ev.groupid = 0 AND ev.courseid = 0 AND ev.categoryid $incategories)"; $subtimesparams = []; if (!empty($subquerytimeconditions)) { $subtimes = $this->subquerytimeconditions("cats", $subquerytimeconditions, $whereparams); $condition .= $subtimes['where']; $subtimesparams = $subtimes['params']; } $subqueryconditions[] = $condition; $subqueryparams = array_merge($subqueryparams, $incategoriesparams, $subtimesparams); } // Build the WHERE condition for the sub-query. if (!empty($subqueryconditions)) { $unionstartquery = "SELECT modulename, instance, eventtype, priority FROM {event} ev WHERE "; $subqueryunion = '('.$unionstartquery . implode(" UNION $unionstartquery ", $subqueryconditions).')'; } else { $subqueryunion = '{event}'; } // Merge subquery parameters to the parameters of the main query. if (!empty($subqueryparams)) { $params = array_merge($params, $subqueryparams); } // Sub-query that fetches the list of unique events that were filtered based on priority. $subquery = "SELECT ev.modulename, ev.instance, ev.eventtype, MIN(ev.priority) as priority FROM $subqueryunion ev GROUP BY ev.modulename, ev.instance, ev.eventtype"; // Build the main query. $sql = "SELECT e.*, c.fullname AS coursefullname, c.shortname AS courseshortname FROM {event} e INNER JOIN ($subquery) fe ON e.modulename = fe.modulename AND e.instance = fe.instance AND e.eventtype = fe.eventtype AND (e.priority = fe.priority OR (e.priority IS NULL AND fe.priority IS NULL)) LEFT JOIN {modules} m ON e.modulename = m.name LEFT JOIN {course} c ON c.id = e.courseid WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause ORDER BY " . ($ordersql ? $ordersql : "e.timestart"); if (!empty($whereparams)) { $params = array_merge($params, $whereparams); } $events = $DB->get_records_sql($sql, $params, $offset, $limitnum); return $events === false ? [] : $events; } /** * Returns a query fragment and params, with time constraints applied * * @param string $prefix * @param array $conditions * @param array $params * @return array [, ] */ protected function subquerytimeconditions(string $prefix, array $conditions, array $params): array { $outwhere = ''; $outparams = []; // Most specific to least specific. $timeparams = ['timefromid', 'timefrom3', 'timefrom2', 'timefrom1', 'timefrom', 'timetoid', 'timeto2', 'timeto1', 'timeto']; $whereconditions = []; foreach ($conditions as $condition) { $where = $condition; // This query has been borrowed from the main WHERE clause, so the alias needs to be renamed to match the union. $where = str_replace('e.id', 'ev.id', $where); foreach ($timeparams as $timeparam) { if (isset($params[$timeparam])) { $where = str_replace(":{$timeparam}", ":{$prefix}{$timeparam}", $where); $outparams["{$prefix}{$timeparam}"] = $params[$timeparam]; } } $whereconditions[] = $where; } if (count($whereconditions) > 0) { $outwhere = ' AND ' . implode(' AND ', $whereconditions); } return ['where' => $outwhere, 'params' => $outparams]; } } PK*\T.m )classes/local/event/proxies/std_proxy.phpnu[. /** * std_proxy class. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\proxies; defined('MOODLE_INTERNAL') || die(); use core_calendar\local\event\exceptions\member_does_not_exist_exception; /** * stdClass proxy. * * This class is intended to proxy things like user, group, etc 'classes' * It will only run the callback to load the object from the DB when necessary. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class std_proxy implements proxy_interface { /** * @var int $id The ID of the database record. */ protected $id; /** * @var \stdClass $class The class we are proxying. */ protected $class; /** * @var callable $callback Callback to run which will load the class to proxy. */ protected $callback; /** * @var array $callbackargs Array of arguments to pass to the callback. */ protected $callbackargs; /** * @var \stdClass $base Base class to get members from. */ protected $base; /** * Constructor. * * @param int $id The ID of the record in the database. * @param callable $callback Callback to load the class. * @param \stdClass $base Class containing base values. */ public function __construct($id, callable $callback, ?\stdClass $base = null) { $this->id = $id; $this->callbackargs = [$id]; $this->callback = $callback; $this->base = $base; } public function get($member) { if ($member === 'id') { return $this->id; } if ($this->base && property_exists($this->base, $member)) { return $this->base->{$member}; } if (!property_exists($this->get_proxied_instance(), $member)) { throw new member_does_not_exist_exception(sprintf('Member %s does not exist', $member)); } return $this->get_proxied_instance()->{$member}; } public function get_proxied_instance() { $callback = $this->callback; return $this->class = $this->class ? $this->class : $callback(...$this->callbackargs); } } PK*\#; -classes/local/event/proxies/cm_info_proxy.phpnu[. /** * Course module cm_info proxy. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\proxies; defined('MOODLE_INTERNAL') || die(); /** * Course module stdClass proxy. * * This implementation differs from the regular std_proxy in that it takes * a module name and instance instead of an id to construct the proxied class. * * This is needed as the event table does not store the id of course modules * instead it stores the module name and instance. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cm_info_proxy implements proxy_interface { /** @var \stdClass */ protected $base; /** @var \cm_info */ protected $cm; /** * cm_info_proxy constructor. * * @param string $modname The module name. * @param int $instance The module instance. * @param int $courseid course id this module belongs to */ public function __construct($modname, $instance, $courseid) { $this->base = (object)['course' => $courseid, 'modname' => $modname, 'instance' => $instance]; } /** * 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 \cm_info */ public function get_proxied_instance() { if (!$this->cm) { $this->cm = get_fast_modinfo($this->base->course)->instances[$this->base->modname][$this->base->instance]; } return $this->cm; } } PK*\PMW/classes/local/event/proxies/proxy_interface.phpnu[. /** * Proxy interface. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\proxies; defined('MOODLE_INTERNAL') || die(); /** * Interface for a proxy class. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface proxy_interface { /** * Retrieve a member of the proxied class. * * @param string $member The name of the member to retrieve * @throws \core_calendar\local\event\exceptions\member_does_not_exist_exception If the proxied class does not have the * requested member. * @return mixed The member. */ public function get($member); /** * Get the full instance of the proxied class. * * @return mixed */ public function get_proxied_instance(); } PK*\ l /classes/local/event/proxies/coursecat_proxy.phpnu[. /** * Course category proxy. * * @package core_calendar * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\proxies; defined('MOODLE_INTERNAL') || die(); /** * Course category proxy. * * This returns an instance of a coursecat rather than a stdClass. * * @copyright 2017 Andrew Nicols * @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 \core_course_category $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 \core_course_category */ public function get_proxied_instance(): \core_course_category { if (!$this->category) { $this->category = \core_course_category::get($this->id, IGNORE_MISSING, true); } return $this->category; } } PK*\!e93737!classes/local/event/container.phpnu[. /** * Core container for calendar events. * * The purpose of this class is simply to wire together the various * implementations of calendar event components to produce a solution * to the problems Moodle core wants to solve. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event; defined('MOODLE_INTERNAL') || die(); use core_calendar\action_factory; use core_calendar\local\event\data_access\event_vault; use core_calendar\local\event\entities\action_event; use core_calendar\local\event\entities\action_event_interface; use core_calendar\local\event\entities\event_interface; use core_calendar\local\event\factories\event_factory; use core_calendar\local\event\mappers\event_mapper; use core_calendar\local\event\strategies\raw_event_retrieval_strategy; /** * Core container. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class container { /** * @var event_factory $eventfactory Event factory. */ protected static $eventfactory; /** * @var event_mapper $eventmapper Event mapper. */ protected static $eventmapper; /** * @var action_factory $actionfactory Action factory. */ protected static $actionfactory; /** * @var event_vault $eventvault Event vault. */ protected static $eventvault; /** * @var raw_event_retrieval_strategy $eventretrievalstrategy Event retrieval strategy. */ protected static $eventretrievalstrategy; /** * @var \stdClass[] An array of cached courses to use with the event factory. */ protected static $coursecache = array(); /** * @var \stdClass[] An array of cached modules to use with the event factory. */ protected static $modulecache = array(); /** * @var int The requesting user. All capability checks are done against this user. */ protected static $requestinguserid; /** * Initialises the dependency graph if it hasn't yet been. */ private static function init() { if (empty(self::$eventfactory)) { self::$actionfactory = new action_factory(); self::$eventmapper = new event_mapper( // The event mapper we return from here needs to know how to // make events, so it needs an event factory. However we can't // give it the same one as we store and return in the container // as that one uses all our plumbing to control event visibility. // // So we make a new even factory that doesn't do anyting other than // return the instance. new event_factory( // Never apply actions, simply return. function(event_interface $event) { return $event; }, // Never hide an event. function() { return true; }, // Never bail out early when instantiating an event. function() { return false; }, self::$coursecache, self::$modulecache ) ); self::$eventfactory = new event_factory( [self::class, 'apply_component_provide_event_action'], [self::class, 'apply_component_is_event_visible'], function ($dbrow) { $requestinguserid = self::get_requesting_user(); if (!empty($dbrow->categoryid)) { // This is a category event. Check that the category is visible to this user. $category = \core_course_category::get($dbrow->categoryid, IGNORE_MISSING, true, $requestinguserid); if (empty($category) || !$category->is_uservisible($requestinguserid)) { return true; } } // For non-module events we assume that all checks were done in core_calendar_is_event_visible callback. // For module events we also check that the course module and course itself are visible to the user. if (empty($dbrow->modulename)) { return false; } $instances = get_fast_modinfo($dbrow->courseid, $requestinguserid)->instances; // If modinfo doesn't know about the module, we should ignore it. if (!isset($instances[$dbrow->modulename]) || !isset($instances[$dbrow->modulename][$dbrow->instance])) { return true; } $cm = $instances[$dbrow->modulename][$dbrow->instance]; // If the module is not visible to the current user, we should ignore it. // We have to check enrolment here as well because the uservisible check // looks for the "view" capability however some activities (such as Lesson) // have that capability set on the "Authenticated User" role rather than // on "Student" role, which means uservisible returns true even when the user // is no longer enrolled in the course. // So, with the following we are checking - // 1) Only process modules if $cm->uservisible is true. // 2) Only process modules for courses a user has the capability to view OR they are enrolled in. // 3) Only process modules for courses that are visible OR if the course is not visible, the user // has the capability to view hidden courses. if (!$cm->uservisible) { return true; } $coursecontext = \context_course::instance($dbrow->courseid); if (!$cm->get_course()->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext, $requestinguserid)) { return true; } if (!has_capability('moodle/course:view', $coursecontext, $requestinguserid) && !is_enrolled($coursecontext, $requestinguserid)) { return true; } // Ok, now check if we are looking at a completion event. if ($dbrow->eventtype === \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED) { // Need to have completion enabled before displaying these events. $course = new \stdClass(); $course->id = $dbrow->courseid; $completion = new \completion_info($course); if ($completion->is_enabled($cm)) { // Check if the event is completed, then in this case we do not need to complete it. // Make sure we're using a cm_info object. $completiondata = $completion->get_data($cm); return intval($completiondata->completionstate) === COMPLETION_COMPLETE; } return true; } return false; }, self::$coursecache, self::$modulecache ); } if (empty(self::$eventvault)) { self::$eventretrievalstrategy = new raw_event_retrieval_strategy(); self::$eventvault = new event_vault(self::$eventfactory, self::$eventretrievalstrategy); } } /** * Reset all static caches, called between tests. */ public static function reset_caches() { self::$requestinguserid = null; self::$eventfactory = null; self::$eventmapper = null; self::$eventvault = null; self::$actionfactory = null; self::$eventretrievalstrategy = null; self::$coursecache = []; self::$modulecache = []; } /** * Gets the event factory. * * @return event_factory */ public static function get_event_factory() { self::init(); return self::$eventfactory; } /** * Gets the event mapper. * * @return event_mapper */ public static function get_event_mapper() { self::init(); return self::$eventmapper; } /** * Return an event vault. * * @return event_vault */ public static function get_event_vault() { self::init(); return self::$eventvault; } /** * Sets the requesting user so that all capability checks are done against this user. * Setting the requesting user (hence calling this function) is optional and if you do not so, * $USER will be used as the requesting user. However, if you wish to set the requesting user yourself, * you should call this function before any other function of the container class is called. * * @param int $userid The user id. * @throws \coding_exception */ public static function set_requesting_user($userid) { self::$requestinguserid = $userid; } /** * Returns the requesting user id. * It usually is the current user unless it has been set explicitly using set_requesting_user. * * @return int */ public static function get_requesting_user() { global $USER; return empty(self::$requestinguserid) ? $USER->id : self::$requestinguserid; } /** * Calls callback 'core_calendar_provide_event_action' from the component responsible for the event * * If no callback is present or callback returns null, there is no action on the event * and it will not be displayed on the dashboard. * * @param event_interface $event * @return action_event|event_interface */ public static function apply_component_provide_event_action(event_interface $event) { // Callbacks will get supplied a "legacy" version // of the event class. $mapper = self::$eventmapper; $action = null; if ($event->get_component()) { $requestinguserid = self::get_requesting_user(); $legacyevent = $mapper->from_event_to_legacy_event($event); // We know for a fact that the the requesting user might be different from the logged in user, // but the event mapper is not aware of that. if (empty($event->user) && !empty($legacyevent->userid)) { $legacyevent->userid = $requestinguserid; } // Any other event will not be displayed on the dashboard. $action = component_callback( $event->get_component(), 'core_calendar_provide_event_action', [ $legacyevent, self::$actionfactory, $requestinguserid ] ); } // If we get an action back, return an action event, otherwise // continue piping through the original event. // // If a module does not implement the callback, component_callback // returns null. return $action ? new action_event($event, $action) : $event; } /** * Calls callback 'core_calendar_is_event_visible' from the component responsible for the event * * The visibility callback is optional, if not present it is assumed as visible. * If it is an actionable event but the get_item_count() returns 0 the visibility * is set to false. * * @param event_interface $event * @return bool */ public static function apply_component_is_event_visible(event_interface $event) { $mapper = self::$eventmapper; $eventvisible = null; if ($event->get_component()) { $requestinguserid = self::get_requesting_user(); $legacyevent = $mapper->from_event_to_legacy_event($event); // We know for a fact that the the requesting user might be different from the logged in user, // but the event mapper is not aware of that. if (empty($event->user) && !empty($legacyevent->userid)) { $legacyevent->userid = $requestinguserid; } $eventvisible = component_callback( $event->get_component(), 'core_calendar_is_event_visible', [ $legacyevent, $requestinguserid ] ); } // Do not display the event if there is nothing to action. if ($event instanceof action_event_interface && $event->get_action()->get_item_count() === 0) { return false; } // Module does not implement the callback, event should be visible. if (is_null($eventvisible)) { return true; } return $eventvisible ? true : false; } } PK*\Wmm,classes/local/event/mappers/event_mapper.phpnu[. /** * Event mapper. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\mappers; defined('MOODLE_INTERNAL') || die(); use core_calendar\event; use core_calendar\local\event\entities\action_event_interface; use core_calendar\local\event\entities\event_interface; use core_calendar\local\event\factories\event_factory_interface; /** * Event mapper class. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_mapper implements event_mapper_interface { /** * @var event_factory_interface $factory Event factory. */ protected $factory; /** * Constructor. * * @param event_factory_interface $factory Event factory. */ public function __construct(event_factory_interface $factory) { $this->factory = $factory; } public function from_legacy_event_to_event(\calendar_event $legacyevent) { $coalesce = function($property) use ($legacyevent) { try { return $legacyevent->$property; } catch (\coding_exception $e) { // The magic setter throews an exception if the // property doesn't exist. return null; } }; return $this->factory->create_instance( (object)[ 'id' => $coalesce('id'), 'name' => $coalesce('name'), 'description' => $coalesce('description'), 'location' => $coalesce('location'), 'format' => $coalesce('format'), 'categoryid' => $coalesce('categoryid'), 'courseid' => $coalesce('courseid'), 'groupid' => $coalesce('groupid'), 'userid' => $coalesce('userid'), 'repeatid' => $coalesce('repeatid'), 'component' => $coalesce('component'), 'modulename' => $coalesce('modulename'), 'instance' => $coalesce('instance'), 'eventtype' => $coalesce('eventtype'), 'timestart' => $coalesce('timestart'), 'timeduration' => $coalesce('timeduration'), 'timemodified' => $coalesce('timemodified'), 'timesort' => $coalesce('timesort'), 'visible' => $coalesce('visible'), 'subscriptionid' => $coalesce('subscriptionid') ] ); } public function from_event_to_legacy_event(event_interface $event) { $action = ($event instanceof action_event_interface) ? $event->get_action() : null; $timeduration = $event->get_times()->get_end_time()->getTimestamp() - $event->get_times()->get_start_time()->getTimestamp(); $properties = $this->from_event_to_stdclass($event); // Normalise for the legacy event because it wants zero rather than null. $properties->courseid = empty($properties->courseid) ? 0 : $properties->courseid; $properties->categoryid = empty($properties->categoryid) ? 0 : $properties->categoryid; $properties->groupid = empty($properties->groupid) ? 0 : $properties->groupid; $properties->userid = empty($properties->userid) ? 0 : $properties->userid; $properties->component = empty($properties->component) ? 0 : $properties->component; $properties->modulename = empty($properties->modulename) ? 0 : $properties->modulename; $properties->instance = empty($properties->instance) ? 0 : $properties->instance; $properties->repeatid = empty($properties->repeatid) ? 0 : $properties->repeatid; return new \calendar_event($properties); } public function from_event_to_stdclass(event_interface $event) { $action = ($event instanceof action_event_interface) ? $event->get_action() : null; $timeduration = $event->get_times()->get_end_time()->getTimestamp() - $event->get_times()->get_start_time()->getTimestamp(); return (object)$this->from_event_to_assoc_array($event); } public function from_event_to_assoc_array(event_interface $event) { $action = ($event instanceof action_event_interface) ? $event->get_action() : null; $timeduration = $event->get_times()->get_end_time()->getTimestamp() - $event->get_times()->get_start_time()->getTimestamp(); return [ 'id' => $event->get_id(), 'name' => $event->get_name(), 'description' => $event->get_description()->get_value(), 'format' => $event->get_description()->get_format(), 'location' => $event->get_location(), 'courseid' => $event->get_course() ? $event->get_course()->get('id') : null, 'categoryid' => $event->get_category() ? $event->get_category()->get('id') : null, 'groupid' => $event->get_group() ? $event->get_group()->get('id') : null, 'userid' => $event->get_user() ? $event->get_user()->get('id') : null, 'repeatid' => $event->get_repeats() ? $event->get_repeats()->get_id() : null, 'component' => $event->get_component(), 'modulename' => $event->get_course_module() ? $event->get_course_module()->get('modname') : null, 'instance' => $event->get_course_module() ? $event->get_course_module()->get('instance') : null, 'eventtype' => $event->get_type(), 'timestart' => $event->get_times()->get_start_time()->getTimestamp(), 'timeduration' => $timeduration, 'timesort' => $event->get_times()->get_sort_time()->getTimestamp(), 'timeusermidnight' => $event->get_times()->get_usermidnight_time()->getTimestamp(), 'visible' => $event->is_visible() ? 1 : 0, 'timemodified' => $event->get_times()->get_modified_time()->getTimestamp(), 'subscriptionid' => $event->get_subscription() ? $event->get_subscription()->get('id') : null, 'actionname' => $action ? $action->get_name() : null, 'actionurl' => $action ? $action->get_url() : null, 'actionnum' => $action ? $action->get_item_count() : null, 'actionactionable' => $action ? $action->is_actionable() : null, 'sequence' => 1 ]; } } PK*\x6classes/local/event/mappers/event_mapper_interface.phpnu[. /** * Event mapper interface. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\mappers; defined('MOODLE_INTERNAL') || die(); use core_calendar\event; use core_calendar\local\event\entities\event_interface; /** * Interface for an event mapper class * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface event_mapper_interface { /** * Map a legacy event to an event. * * @param \calendar_event $event The legacy event. * @return event_interface The mapped event. */ public function from_legacy_event_to_event(\calendar_event $event); /** * Map an event to a legacy event. * * @param event_interface $event The legacy event. * @return \calendar_event The mapped legacy event. */ public function from_event_to_legacy_event(event_interface $event); /** * Map an event to a stdClass * * @param event_interface $event The legacy event. * @return \stdClass The mapped stdClass. */ public function from_event_to_stdclass(event_interface $event); /** * Map an event to an associative array. * * @param event_interface $event The legacy event. * @return array The mapped legacy event array. */ public function from_event_to_assoc_array(event_interface $event); } PK*\Cclasses/local/event/mappers/create_update_form_mapper_interface.phpnu[. /** * Create update form mapper interface. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\mappers; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/calendar/lib.php'); /** * Interface for a create_update_form_mapper class * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface create_update_form_mapper_interface { /** * Generate the appropriate data for the form from a legacy event. * * @param \calendar_event $legacyevent * @return stdClass */ public function from_legacy_event_to_data(\calendar_event $legacyevent); /** * Generate the appropriate calendar_event properties from the form data. * * @param \stdClass $data * @return stdClass */ public function from_data_to_event_properties(\stdClass $data); } PK*\9classes/local/event/mappers/create_update_form_mapper.phpnu[. /** * Event create form and update form mapper. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\mappers; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/calendar/lib.php'); /** * Event create form and update form mapper class. * * This class will perform the necessary data transformations to take * a legacy event and build the appropriate data structure for both the * create and update event forms. * * It will also do the reverse transformation * and take the returned form data and provide a data structure that can * be used to set legacy event properties. * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class create_update_form_mapper implements create_update_form_mapper_interface { /** * Generate the appropriate data for the form from a legacy event. * * @param \calendar_event $legacyevent * @return stdClass */ public function from_legacy_event_to_data(\calendar_event $legacyevent) { $legacyevent->count_repeats(); $data = $legacyevent->properties(); $data->timedurationuntil = $legacyevent->timestart + $legacyevent->timeduration; $data->duration = (empty($legacyevent->timeduration)) ? 0 : 1; if ($legacyevent->eventtype == 'group') { // Set up the correct value for the to display on the form. $data->groupid = $legacyevent->groupid; $data->groupcourseid = $legacyevent->courseid; } if ($legacyevent->eventtype == 'course') { // Set up the correct value for the to display on the form. $data->courseid = $legacyevent->courseid; } $data->description = [ 'text' => $data->description, 'format' => $data->format ]; // Don't return context or subscription because they're not form values and break validation. if (isset($data->context)) { unset($data->context); } if (isset($data->subscription)) { unset($data->subscription); } return $data; } /** * Generate the appropriate calendar_event properties from the form data. * * @param \stdClass $data * @return stdClass */ public function from_data_to_event_properties(\stdClass $data) { $properties = clone($data); if ($data->eventtype == 'group') { if (isset($data->groupcourseid)) { $properties->courseid = $data->groupcourseid; unset($properties->groupcourseid); } if (isset($data->groupid)) { $properties->groupid = $data->groupid; } } else { // Default course id if none is set. if (empty($properties->courseid)) { if ($properties->eventtype == 'site') { $properties->courseid = SITEID; } else { $properties->courseid = 0; } } else { $properties->courseid = $data->courseid; } if (empty($properties->groupid)) { $properties->groupid = 0; } } // Decode the form fields back into valid event property. $properties->timeduration = $this->get_time_duration_from_form_data($data); return $properties; } /** * A helper function to calculate the time duration for an event based on * the event_form data. * * @param \stdClass $data event_form data * @return int */ private function get_time_duration_from_form_data(\stdClass $data) { if ($data->duration == 1) { return $data->timedurationuntil - $data->timestart; } else if ($data->duration == 2) { return $data->timedurationminutes * MINSECS; } else { return 0; } } } PK*\V1$classes/local/event/forms/update.phpnu[. /** * The mform for updating a calendar event. Based on the old event form. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\forms; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/lib/formslib.php'); /** * The mform class for updating a calendar event. * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class update extends create { /** * Add the repeat elements for the form when editing an existing event. * * @param MoodleQuickForm $mform */ protected function add_event_repeat_elements($mform) { $event = $this->_customdata['event']; $mform->addElement('hidden', 'repeatid'); $mform->setType('repeatid', PARAM_INT); if (!empty($event->repeatid)) { $group = []; $group[] = $mform->createElement('radio', 'repeateditall', null, get_string('repeateditall', 'calendar', $event->eventrepeats), 1); $group[] = $mform->createElement('radio', 'repeateditall', null, get_string('repeateditthis', 'calendar'), 0); $mform->addGroup($group, 'repeatgroup', get_string('repeatedevents', 'calendar'), '
', false); $mform->setDefault('repeateditall', 1); $mform->setAdvanced('repeatgroup'); } } } PK*\:W  'classes/local/event/forms/eventtype.phpnu[. /** * The trait for adding eventtype fields to a form. * * @package core_calendar * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\forms; defined('MOODLE_INTERNAL') || die(); /** * The trait for adding eventtype fields to a form. * * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait eventtype { /** * Add the appropriate elements for the available event types. * * If the only event type available is 'user' then we add a hidden * element because there is nothing for the user to choose. * * If more than one type is available then we add the elements as * follows: * - Always add the event type selector * - Elements per type: * - course: add an additional select element with each * course as an option. * - group: add a select element for the course (different * from the above course select) and a select * element for the group. * * @param MoodleQuickForm $mform * @param array $eventtypes The available event types for the user */ protected function add_event_type_elements($mform, $eventtypes) { global $CFG, $DB; $options = []; if (!empty($eventtypes['user'])) { $options['user'] = get_string('user', 'calendar'); } if (!empty($eventtypes['group'])) { $options['group'] = get_string('group', 'calendar'); } if (!empty($eventtypes['course'])) { $options['course'] = get_string('course', 'calendar'); } if (!empty($eventtypes['category'])) { $options['category'] = get_string('category', 'calendar'); } if (!empty($eventtypes['site'])) { $options['site'] = get_string('site', 'calendar'); } // If we only have one event type and it's 'user' event then don't bother // rendering the select boxes because there is no choice for the user to // make. if (!empty($eventtypes['user']) && count($options) == 1) { $mform->addElement('hidden', 'eventtype'); $mform->setType('eventtype', PARAM_TEXT); $mform->hardFreeze('eventtype'); $mform->setConstant('eventtype', 'user'); return; } else { $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options); } if (!empty($eventtypes['category'])) { $categoryoptions = []; foreach (\core_course_category::make_categories_list('moodle/category:manage') as $id => $category) { $categoryoptions[$id] = $category; } $mform->addElement('autocomplete', 'categoryid', get_string('category'), $categoryoptions); $mform->hideIf('categoryid', 'eventtype', 'noteq', 'category'); } $showall = is_siteadmin() && !empty($CFG->calendar_adminseesall); if (!empty($eventtypes['course'])) { $mform->addElement('course', 'courseid', get_string('course'), ['limittoenrolled' => !$showall]); $mform->hideIf('courseid', 'eventtype', 'noteq', 'course'); } if (!empty($eventtypes['group'])) { $groups = !(empty($this->_customdata['groups'])) ? $this->_customdata['groups'] : null; // Get the list of courses without groups to filter on the course selector. $sql = "SELECT c.id FROM {course} c WHERE c.id NOT IN ( SELECT DISTINCT courseid FROM {groups} )"; $coursesnogroup = $DB->get_records_sql($sql); $mform->addElement('course', 'groupcourseid', get_string('course'), ['limittoenrolled' => !$showall, 'exclude' => array_keys($coursesnogroup)]); $mform->hideIf('groupcourseid', 'eventtype', 'noteq', 'group'); $mform->addElement('select', 'groupid', get_string('group'), $groups); $mform->hideIf('groupid', 'eventtype', 'noteq', 'group'); // We handle the group select hide/show actions on the event_form module. } } } PK*\/1classes/local/event/forms/managesubscriptions.phpnu[. /** * Allows the user to manage calendar subscriptions. * * @copyright 2012 Jonathan Harker * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package calendar */ namespace core_calendar\local\event\forms; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); /** * Form for adding a subscription to a Moodle course calendar. * @copyright 2012 Jonathan Harker * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class managesubscriptions extends \moodleform { use eventtype; /** * Defines the form used to add calendar subscriptions. */ public function definition() { global $PAGE; $mform = $this->_form; $eventtypes = calendar_get_allowed_event_types(); if (in_array(true, $eventtypes, true) === false) { throw new \moodle_exception('nopermissiontoupdatecalendar'); } // Name. $mform->addElement('text', 'name', get_string('subscriptionname', 'calendar'), array('maxsize' => '255', 'size' => '40')); $mform->addRule('name', get_string('required'), 'required', null, 'client'); $mform->setType('name', PARAM_TEXT); // Import from (url | importfile). $choices = array(CALENDAR_IMPORT_FROM_FILE => get_string('importfromfile', 'calendar'), CALENDAR_IMPORT_FROM_URL => get_string('importfromurl', 'calendar')); $mform->addElement('select', 'importfrom', get_string('importcalendarfrom', 'calendar'), $choices); $mform->setDefault('importfrom', CALENDAR_IMPORT_FROM_URL); // URL. $mform->addElement('text', 'url', get_string('importfromurl', 'calendar'), array('maxsize' => '255', 'size' => '50')); // Cannot set as PARAM_URL since we need to allow webcal:// protocol. $mform->setType('url', PARAM_RAW); $mform->setForceLtr('url'); // Poll interval $choices = calendar_get_pollinterval_choices(); $mform->addElement('select', 'pollinterval', get_string('pollinterval', 'calendar'), $choices); $mform->setDefault('pollinterval', 604800); $mform->addHelpButton('pollinterval', 'pollinterval', 'calendar'); $mform->setType('pollinterval', PARAM_INT); // Import file $mform->addElement('filepicker', 'importfile', get_string('importfromfile', 'calendar'), null, array('accepted_types' => '.ics')); // Disable appropriate elements depending on import from value. $mform->hideIf('pollinterval', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_FILE); $mform->hideIf('url', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_FILE); $mform->hideIf('importfile', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_URL); // Add the select elements for the available event types. $this->add_event_type_elements($mform, $eventtypes); // Eventtype: 0 = user, 1 = site, anything else = course ID. $mform->addElement('submit', 'add', get_string('importcalendar', 'calendar')); // Add the javascript required to enhance this mform. $PAGE->requires->js_call_amd('core_calendar/event_form', 'init', [$mform->getAttribute('id')]); } /** * Validates the returned data. * * @param array $data * @param array $files * @return array */ public function validation($data, $files) { global $USER; $errors = parent::validation($data, $files); $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null; $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid'; $courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null; $eventtypes = calendar_get_allowed_event_types($courseid); if (empty($eventtype) || !isset($eventtypes[$eventtype]) || $eventtypes[$eventtype] == false) { $errors['eventtype'] = get_string('invalideventtype', 'calendar'); } if ($data['importfrom'] == CALENDAR_IMPORT_FROM_FILE) { if (empty($data['importfile'])) { $errors['importfile'] = get_string('errorrequiredurlorfile', 'calendar'); } else { // Make sure the file area is not empty and contains only one file. $draftitemid = $data['importfile']; $fs = get_file_storage(); $usercontext = \context_user::instance($USER->id); $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id DESC', false); if (count($files) !== 1) { $errors['importfile'] = get_string('errorrequiredurlorfile', 'calendar'); } } } else if (($data['importfrom'] == CALENDAR_IMPORT_FROM_URL)) { if (empty($data['url'])) { $errors['url'] = get_string('errorrequiredurlorfile', 'calendar'); } else { // Clean input calendar url. $url = clean_param($data['url'], PARAM_URL); try { calendar_get_icalendar($url); } catch (\moodle_exception $e) { $errors['url'] = get_string('errorinvalidicalurl', 'calendar'); } } } else { // Shouldn't happen. $errors['url'] = get_string('errorrequiredurlorfile', 'calendar'); } // Validate course/category event types (ensure appropriate field is also filled in). if ($eventtype === 'course' && empty($data['courseid'])) { $errors['courseid'] = get_string('selectacourse'); } else if ($eventtype === 'category' && empty($data['categoryid'])) { $errors['categoryid'] = get_string('required'); } return $errors; } public function definition_after_data() { $mform =& $this->_form; $mform->applyFilter('url', static::class . '::strip_webcal'); $mform->applyFilter('url', 'trim'); } /** * Replace webcal:// urls with http:// as * curl does not understand this protocol * * @param string @url url to examine * @return string url with webcal:// replaced */ public static function strip_webcal($url) { if (strpos($url, 'webcal://') === 0) { $url = str_replace('webcal://', 'http://', $url); } return $url; } } PK*\gC''$classes/local/event/forms/create.phpnu[. /** * The mform for creating a calendar event. Based on the old event form. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\forms; use context_system; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/lib/formslib.php'); /** * The mform class for creating a calendar event. * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class create extends \moodleform { use eventtype; /** * Build the editor options using the given context. * * @param \context $context A Moodle context * @return array */ public static function build_editor_options(\context $context) { global $CFG; return [ 'context' => $context, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $CFG->maxbytes, 'noclean' => true, 'autosave' => false ]; } /** * The form definition */ public function definition() { global $PAGE; $mform = $this->_form; $starttime = isset($this->_customdata['starttime']) ? $this->_customdata['starttime'] : 0; $editoroptions = !(empty($this->_customdata['editoroptions'])) ? $this->_customdata['editoroptions'] : null; $courseid = !(empty($this->_customdata['courseid'])) ? $this->_customdata['courseid'] : null; $eventtypes = $this->_customdata['eventtypes']; if (in_array(true, $eventtypes, true) === false) { throw new \moodle_exception('nopermissiontoupdatecalendar'); } $mform->setDisableShortforms(); $mform->disable_form_change_checker(); // Empty string so that the element doesn't get rendered. $mform->addElement('header', 'general', ''); $this->add_default_hidden_elements($mform); // Event name field. $mform->addElement('text', 'name', get_string('eventname', 'calendar'), 'size="50"'); $mform->addRule('name', get_string('required'), 'required', null, 'client'); $mform->setType('name', PARAM_TEXT); // Event time start field. $mform->addElement('date_time_selector', 'timestart', get_string('date'), ['defaulttime' => $starttime]); // Add the select elements for the available event types. $this->add_event_type_elements($mform, $eventtypes); // Start of advanced elements. // Advanced elements are not visible to the user by default. // They are displayed through the user of a show more / less button. $mform->addElement('editor', 'description', get_string('eventdescription', 'calendar'), ['rows' => 3], $editoroptions); $mform->setType('description', PARAM_RAW); $mform->setAdvanced('description'); $mform->addElement('text', 'location', get_string('location', 'moodle'), 'size="50"'); $mform->setType('location', PARAM_RAW_TRIMMED); $mform->setAdvanced('location'); // Add the variety of elements allowed for selecting event duration. $this->add_event_duration_elements($mform); // Add the form elements for repeating events. $this->add_event_repeat_elements($mform); // Add the javascript required to enhance this mform. $PAGE->requires->js_call_amd('core_calendar/event_form', 'init', [$mform->getAttribute('id')]); } /** * A bit of custom validation for this form * * @param array $data An assoc array of field=>value * @param array $files An array of files * @return array */ public function validation($data, $files) { global $DB; $errors = parent::validation($data, $files); $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null; $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid'; $courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null; $categoryid = (!empty($data['categoryid'])) ? $data['categoryid'] : null; $eventtypes = $this->_customdata['eventtypes']; if (empty($eventtype) || !isset($eventtypes[$eventtype]) || $eventtypes[$eventtype] == false) { $errors['eventtype'] = get_string('invalideventtype', 'calendar'); } if ($courseid && $courseid > 0) { if ($course = $DB->get_record('course', ['id' => $courseid])) { if ($data['timestart'] < $course->startdate) { $errors['timestart'] = get_string('errorbeforecoursestart', 'calendar'); } } else { $errors[$coursekey] = get_string('invalidcourse', 'error'); } } if ($eventtype == 'course' && empty($courseid)) { $errors['courseid'] = get_string('selectacourse'); } if ($eventtype == 'category' && empty($categoryid)) { $errors['categoryid'] = get_string('selectacategory'); } if ($eventtype == 'group' && (!empty($courseid) && empty($data['groupid']))) { $errors['groupcourseid'] = get_string('nogroups', 'core_group'); } if ($eventtype == 'group' && empty($courseid)) { $errors['groupcourseid'] = get_string('selectacourse'); } if ($data['duration'] == 1 && $data['timestart'] > $data['timedurationuntil']) { $errors['durationgroup'] = get_string('invalidtimedurationuntil', 'calendar'); } else if ($data['duration'] == 2 && (trim($data['timedurationminutes']) == '' || $data['timedurationminutes'] < 1)) { $errors['durationgroup'] = get_string('invalidtimedurationminutes', 'calendar'); } return $errors; } /** * Add the list of hidden elements that should appear in this form each * time. These elements will never be visible to the user. * * @param MoodleQuickForm $mform */ protected function add_default_hidden_elements($mform) { global $USER; // Add some hidden fields. $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->setDefault('id', 0); $mform->addElement('hidden', 'userid'); $mform->setType('userid', PARAM_INT); $mform->setDefault('userid', $USER->id); $mform->addElement('hidden', 'modulename'); $mform->setType('modulename', PARAM_INT); $mform->setDefault('modulename', ''); $mform->addElement('hidden', 'instance'); $mform->setType('instance', PARAM_INT); $mform->setDefault('instance', 0); $mform->addElement('hidden', 'visible'); $mform->setType('visible', PARAM_INT); $mform->setDefault('visible', 1); } /** * Add the various elements to express the duration options available * for an event. * * @param MoodleQuickForm $mform */ protected function add_event_duration_elements($mform) { $group = []; $group[] = $mform->createElement('radio', 'duration', null, get_string('durationnone', 'calendar'), 0); $group[] = $mform->createElement('radio', 'duration', null, get_string('durationuntil', 'calendar'), 1); $group[] = $mform->createElement('date_time_selector', 'timedurationuntil', ''); $group[] = $mform->createElement('radio', 'duration', null, get_string('durationminutes', 'calendar'), 2); $group[] = $mform->createElement('text', 'timedurationminutes', get_string('durationminutes', 'calendar'), 'maxlength="7" size="7"'); $mform->addGroup($group, 'durationgroup', get_string('eventduration', 'calendar'), '
', false); $mform->setAdvanced('durationgroup'); $mform->disabledIf('timedurationuntil', 'duration', 'noteq', 1); $mform->disabledIf('timedurationuntil[day]', 'duration', 'noteq', 1); $mform->disabledIf('timedurationuntil[month]', 'duration', 'noteq', 1); $mform->disabledIf('timedurationuntil[year]', 'duration', 'noteq', 1); $mform->disabledIf('timedurationuntil[hour]', 'duration', 'noteq', 1); $mform->disabledIf('timedurationuntil[minute]', 'duration', 'noteq', 1); $mform->setType('timedurationminutes', PARAM_INT); $mform->disabledIf('timedurationminutes', 'duration', 'noteq', 2); $mform->addGroupRule('durationgroup', [ 'timedurationminutes' => [[get_string('maximumchars', '', 7), 'maxlength', 7, 'server']], ]); $mform->setDefault('duration', 0); } /** * Add the repeat elements for the form when creating a new event. * * @param MoodleQuickForm $mform */ protected function add_event_repeat_elements($mform) { $mform->addElement('checkbox', 'repeat', get_string('repeatevent', 'calendar'), null); $mform->addElement('text', 'repeats', get_string('repeatweeksl', 'calendar'), 'maxlength="3" size="3"'); $mform->setType('repeats', PARAM_INT); $mform->setDefault('repeats', 1); $mform->disabledIf('repeats', 'repeat', 'notchecked'); $mform->setAdvanced('repeat'); $mform->setAdvanced('repeats'); $mform->addRule('repeats', get_string('maximumchars', '', 3), 'maxlength', 3, 'server'); } } PK*\8classes/local/event/entities/repeat_event_collection.phpnu[. /** * Event collection class. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\entities; defined('MOODLE_INTERNAL') || die(); use core_calendar\local\event\factories\event_factory_interface; /** * Class representing a collection of repeat events. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repeat_event_collection implements event_collection_interface { /** * @var int DB_QUERY_LIMIT How many records to pull from the DB at once. */ const DB_QUERY_LIMIT = 100; /** * @var int $parentid The ID of the event which the events in this collection are repeats of. */ protected $parentid; /** * @var \stdClass $parentrecord The parent event record from the database. */ protected $parentrecord; /** * @var event_factory_interface $factory Event factory. */ protected $factory; /** * @var int $num Total number of events that could be retrieved by this collection. */ protected $num; /** * Constructor. * * @param stdClass $dbrow The event dbrow that is being repeated. * @param event_factory_interface $factory Event factory. */ public function __construct($dbrow, event_factory_interface $factory) { $eventid = $dbrow->id; $repeatid = $dbrow->repeatid; if (empty($repeatid)) { $this->parentrecord = $dbrow; $this->parentid = $eventid; } else { $this->parentid = $repeatid; } if ($eventid === $repeatid) { // This means the record we've been given is the parent // record. $this->parentrecord = $dbrow; } $this->factory = $factory; } public function get_id() { return $this->parentid; } public function get_num() { global $DB; // Subtract one because the original event has repeatid = its own id. return $this->num = max( isset($this->num) ? $this->num : ($DB->count_records('event', ['repeatid' => $this->parentid]) - 1), 0 ); } public function getIterator(): \Traversable { $parentrecord = $this->get_parent_record(); foreach ($this->load_event_records() as $eventrecords) { foreach ($eventrecords as $eventrecord) { // In the case of the repeat event having unset information, fallback on the parent. yield $this->factory->create_instance((object)array_merge((array)$parentrecord, (array)$eventrecord)); } } } /** * Return the parent DB record. * * @return \stdClass */ protected function get_parent_record() { global $DB; if (!isset($this->parentrecord)) { $this->parentrecord = $DB->get_record('event', ['id' => $this->parentid]); } return $this->parentrecord; } /** * Generate more event records. * * @param int $start Start offset. * @return \stdClass[] */ protected function load_event_records($start = 0) { global $DB; while ($records = $DB->get_records_select( 'event', 'id <> :parentid AND repeatid = :repeatid', [ 'parentid' => $this->parentid, 'repeatid' => $this->parentid, ], 'id ASC', '*', $start, self::DB_QUERY_LIMIT )) { yield $records; $start += self::DB_QUERY_LIMIT; } } } PK*\f(;classes/local/event/entities/event_collection_interface.phpnu[. /** * Interface for an event collection class. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\entities; defined('MOODLE_INTERNAL') || die(); /** * Interface for an event collection class. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface event_collection_interface extends \IteratorAggregate { /** * Get the event collection's ID. * * @return int */ public function get_id(); /** * Get the total number of repeats in the collection. * * @return int */ public function get_num(); } PK*\&classes/local/event/entities/event.phpnu[. /** * Calendar event class. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\entities; defined('MOODLE_INTERNAL') || die(); use core_calendar\local\event\proxies\proxy_interface; use core_calendar\local\event\value_objects\description_interface; use core_calendar\local\event\value_objects\times_interface; /** * Class representing a calendar event. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event implements event_interface { /** * @var int $id The event's id in the database. */ protected $id; /** * @var string $name The name of this event. */ protected $name; /** * @var description_interface $description Description for this event. */ protected $description; /** * @var string $location Location of this event. */ protected $location; /** * @var proxy_interface $category Category for this event. */ protected $category; /** * @var proxy_interface $course Course for this event. */ protected $course; /** * @var proxy_interface $group Group for this event. */ protected $group; /** * @var proxy_interface $user User for this event. */ protected $user; /** * @var event_collection_interface $repeats Collection of repeat events. */ protected $repeats; /** * @var proxy_interface $coursemodule The course module that created this event. */ protected $coursemodule; /** * @var string type The type of this event. */ protected $type; /** * @var times_interface $times The times for this event. */ protected $times; /** * @var bool $visible The visibility of this event. */ protected $visible; /** * @var string $component */ protected $component; /** * @var proxy_interface $subscription Subscription for this event. */ protected $subscription; /** * Constructor. * * @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|null $category The category associated with the event. * @param proxy_interface|null $course The course associated with the event. * @param proxy_interface|null $group The group associated with the event. * @param proxy_interface|null $user The user associated with the event. * @param event_collection_interface|null $repeats Collection of repeat events. * @param proxy_interface|null $coursemodule The course module that created the event. * @param string $type The event's type. * @param times_interface $times The times associated with the event. * @param bool $visible The event's visibility. True for visible, false for invisible. * @param proxy_interface|null $subscription The event's subscription. * @param string $location The event's location. * @param string $component The event's component. */ public function __construct( $id, $name, description_interface $description, ?proxy_interface $category, ?proxy_interface $course, ?proxy_interface $group, ?proxy_interface $user, ?event_collection_interface $repeats, ?proxy_interface $coursemodule, $type, times_interface $times, $visible, ?proxy_interface $subscription = null, $location = null, $component = null ) { $this->id = $id; $this->name = $name; $this->description = $description; $this->location = $location; $this->category = $category; $this->course = $course; $this->group = $group; $this->user = $user; $this->repeats = $repeats; $this->coursemodule = $coursemodule; $this->type = $type; $this->times = $times; $this->visible = $visible; $this->subscription = $subscription; $this->component = $component; } public function get_id() { return $this->id; } public function get_name() { return $this->name; } public function get_description() { return $this->description; } public function get_location() { return $this->location; } public function get_category() { return $this->category; } public function get_course() { return $this->course; } public function get_course_module() { return $this->coursemodule; } public function get_group() { return $this->group; } public function get_user() { return $this->user; } public function get_type() { return $this->type; } public function get_times() { return $this->times; } public function get_repeats() { return $this->repeats; } public function get_subscription() { return $this->subscription; } public function is_visible() { return $this->visible; } /** * Resolved event component (frankenstyle name of activity module or the component) * @return string|null */ public function get_component() { return $this->get_course_module() ? 'mod_' . $this->get_course_module()->get('modname') : $this->component; } } PK*\n  0classes/local/event/entities/event_interface.phpnu[. /** * Calendar event interface. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\entities; use core_calendar\local\event\proxies\proxy_interface; defined('MOODLE_INTERNAL') || die(); /** * Interface for an event class. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface event_interface { /** * Get the event's ID. * * @return integer */ public function get_id(); /** * Get the event's name. * * @return string */ public function get_name(); /** * Get the event's description. * * @return description_interface */ public function get_description(); /** * Get the event's location. * * @return location_interface */ public function get_location(); /** * Get the category object associated with the event. * * @return proxy_interface */ public function get_category(); /** * Get the course object associated with the event. * * @return proxy_interface */ public function get_course(); /** * Get the course module object that created the event. * * @return proxy_interface */ public function get_course_module(); /** * Get the group object associated with the event. * * @return proxy_interface */ public function get_group(); /** * Get the user object associated with the event. * * @return proxy_interface */ public function get_user(); /** * Get the event's type. * * @return string */ public function get_type(); /** * Get the times associated with the event. * * @return times_interface */ public function get_times(); /** * Get repeats of this event or null if the event has no * repeats. * * @return event_collection_interface|null */ public function get_repeats(); /** * Get the event's subscription. * * @return proxy_interface */ public function get_subscription(); /** * Get the event's visibility. * * @return bool true if the event is visible, false otherwise */ public function is_visible(); /** * Resolved event component (frankenstyle name of activity module or the component) * @return string|null */ public function get_component(); } PK*\hdd1classes/local/event/entities/action_interface.phpnu[. /** * Action interface. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\entities; defined('MOODLE_INTERNAL') || die(); /** * Interface for a action class. * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface action_interface { /** * Get the name of the action. * * @return string */ public function get_name(); /** * Get the URL of the action. * * @return \moodle_url */ public function get_url(); /** * Get the number of items that need actioning. * * @return int */ public function get_item_count(); /** * Get the actions actionability. * * @return bool */ public function is_actionable(); } PK*\KK-classes/local/event/entities/action_event.phpnu[. /** * Calendar action event class. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\entities; defined('MOODLE_INTERNAL') || die(); use core_calendar\local\event\factories\action_factory_interface; /** * Class representing an actionable event. * * An actionable event can be thought of as an embellished event. That is, * it does everything a regular event does, but has some extra information * attached to it. For example, the URL a user needs to visit to complete * an action, the number of actionable items, etc. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class action_event implements action_event_interface { /** * @var event_interface $event The event to delegate to. */ protected $event; /** * @var action_interface $action The action associated with this event. */ protected $action; /** * @var proxy_interface $category Category for this event. */ protected $category; /** * Constructor. * * @param event_interface $event The event to delegate to. * @param action_interface $action The action associated with this event. */ public function __construct(event_interface $event, action_interface $action) { $this->event = $event; $this->action = $action; } public function get_id() { return $this->event->get_id(); } public function get_name() { return $this->event->get_name(); } public function get_description() { return $this->event->get_description(); } public function get_location() { return $this->event->get_location(); } public function get_category() { return $this->event->get_category(); } public function get_course() { return $this->event->get_course(); } public function get_course_module() { return $this->event->get_course_module(); } public function get_group() { return $this->event->get_group(); } public function get_user() { return $this->event->get_user(); } public function get_type() { return $this->event->get_type(); } public function get_times() { return $this->event->get_times(); } public function get_repeats() { return $this->event->get_repeats(); } public function get_subscription() { return $this->event->get_subscription(); } public function is_visible() { return $this->event->is_visible(); } public function get_action() { return $this->action; } /** * Event component * @return string */ public function get_component() { return $this->event->get_component(); } } PK*\wҥEII7classes/local/event/entities/action_event_interface.phpnu[. /** * Calendar action event interface. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\entities; defined('MOODLE_INTERNAL') || die(); /** * Interface for an action event class. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface action_event_interface extends event_interface { /** * Get the action event's action. * * @return action_interface */ public function get_action(); } PK*\ ,classes/local/event/value_objects/action.phpnu[. /** * Class representing an action a user should take. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\value_objects; defined('MOODLE_INTERNAL') || die(); use core_calendar\local\event\entities\action_interface; /** * Class representing an action a user should take * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class action implements action_interface { /** * @var string $name The action's name. */ protected $name; /** * @var \moodle_url $url The action's URL. */ protected $url; /** * @var int $itemcount How many items there are to action. */ protected $itemcount; /** * @var bool $actionable Whether or not the event is currently actionable. */ protected $actionable; /** * Constructor. * * @param string $name The action's name. * @param \moodle_url $url The action's URL. * @param int $itemcount How many items there are to action. * @param bool $actionable Whether or not the event is currently actionable. */ public function __construct( $name, \moodle_url $url, $itemcount, $actionable ) { $this->name = $name; $this->url = $url; $this->itemcount = $itemcount; $this->actionable = $actionable; } public function get_name() { return $this->name; } public function get_url() { return $this->url; } public function get_item_count() { return $this->itemcount; } public function is_actionable() { return $this->actionable; } } PK*\,;classes/local/event/value_objects/description_interface.phpnu[. /** * Description value object interface. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\value_objects; defined('MOODLE_INTERNAL') || die(); /** * Interface for a description value object. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface description_interface { /** * Get the description's text. * * @return string The description's text. */ public function get_value(); /** * Get the description's format. * * @return int The description's format. */ public function get_format(); } PK*\U5classes/local/event/value_objects/times_interface.phpnu[. /** * Times interface. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\value_objects; defined('MOODLE_INTERNAL') || die(); /** * Interface for various times. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface times_interface { /** * Get the start time. * * @return \DateTimeImmutable */ public function get_start_time(); /** * Get the end time. * * @return \DateTimeImmutable */ public function get_end_time(); /** * Get the duration (the time between start and end). * * @return \DateInterval */ public function get_duration(); /** * Get the sort time. * * @return \DateTimeImmutable */ public function get_sort_time(); /** * Get the modified time. * * @return \DateTimeImmutable */ public function get_modified_time(); /** * Get the user midnight time. * * @return \DateTimeImmutable */ public function get_usermidnight_time(); } PK*\  1classes/local/event/value_objects/event_times.phpnu[. /** * Event times class. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\value_objects; defined('MOODLE_INTERNAL') || die(); /** * Class representing event times. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_times implements times_interface { /** * @var \DateTimeImmutable $start Event start time. */ protected $start; /** * @var \DateTimeImmutable $end Event end time. */ protected $end; /** * @var \DateTimeImmutable $sort Time used to sort events. */ protected $sort; /** * @var \DateTimeImmutable $modified Time event was last modified. */ protected $modified; /** * @var \DateTimeImmutable $usermidnight User midnight for the event. */ protected $usermidnight; /** * Constructor. * * @param \DateTimeImmutable $start Event start time. * @param \DateTimeImmutable $end Event end time. * @param \DateTimeImmutable $sort Date used to sort events. * @param \DateTimeImmutable $modified Time event was last updated. * @param \DateTimeImmutable $usermidnight User midnight for the event. */ public function __construct( \DateTimeImmutable $start, \DateTimeImmutable $end, \DateTimeImmutable $sort, \DateTimeImmutable $modified, \DateTimeImmutable $usermidnight ) { $this->start = $start; $this->end = $end; $this->sort = $sort; $this->modified = $modified; $this->usermidnight = $usermidnight; } public function get_start_time() { return $this->start; } public function get_end_time() { return $this->end; } public function get_duration() { return $this->end->diff($this->start); } public function get_modified_time() { return $this->modified; } public function get_sort_time() { return $this->sort; } /** * Getter for usermidnight. * * @return \DateTimeImmutable */ public function get_usermidnight_time() { return $this->usermidnight; } } PK*\r}  7classes/local/event/value_objects/event_description.phpnu[. /** * Description value object. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\value_objects; defined('MOODLE_INTERNAL') || die(); /** * Class representing a description value object. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_description implements description_interface { /** * @var string $value The description's text. */ protected $value; /** * @var int $format The description's format. */ protected $format; /** * Constructor. * * @param string $value The description's value. * @param int $format The description's format. */ public function __construct($value, $format) { $this->value = $value; $this->format = $format; } public function get_value() { return $this->value; } public function get_format() { return $this->format; } } PK*\Wczz/classes/local/event/factories/event_factory.phpnu[. /** * Event factory class. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\factories; defined('MOODLE_INTERNAL') || die(); use core_calendar\local\event\exceptions\invalid_callback_exception; use core_calendar\local\event\entities\event_interface; /** * Event factory class. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_factory extends event_abstract_factory { protected function apply_component_action(event_interface $event) { $callbackapplier = $this->actioncallbackapplier; $callbackresult = $callbackapplier($event); if (!$callbackresult instanceof event_interface) { throw new invalid_callback_exception( 'Event factory action callback applier must return an instance of event_interface'); } return $callbackresult; } protected function expose_event(event_interface $event) { $callbackapplier = $this->visibilitycallbackapplier; $callbackresult = $callbackapplier($event); if (!is_bool($callbackresult)) { throw new invalid_callback_exception('Event factory visibility callback applier must return true or false'); } return $callbackresult === true ? $event : null; } } PK*\P\9classes/local/event/factories/event_factory_interface.phpnu[. /** * Event factory interface. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\factories; defined('MOODLE_INTERNAL') || die(); /** * Interface for an event factory class. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface event_factory_interface { /** * Creates an instance of an event. * * @param \stdClass $dbrow The event row from the database. * @return \core_calendar\local\event\entities\event_interface */ public function create_instance(\stdClass $dbrow); } PK*\.:classes/local/event/factories/action_factory_interface.phpnu[. /** * Action factory interface. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\factories; defined('MOODLE_INTERNAL') || die(); interface action_factory_interface { /** * Creates an instance of an action. * * @param string $name The action's name. * @param \moodle_url $url The action's URL. * @param int $itemcount The number of items needing action. * @param bool $actionable The action's actionability. * @return \core_calendar\local\event\entities\action_interface The action. */ public function create_instance($name, \moodle_url $url, $itemcount, $actionable); } PK*\o8classes/local/event/factories/event_abstract_factory.phpnu[. /** * Abstract event factory. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\factories; defined('MOODLE_INTERNAL') || die(); use core_calendar\local\event\entities\event; 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; /** * Abstract factory for creating calendar events. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class event_abstract_factory implements event_factory_interface { /** * @var callable $actioncallbackapplier Function to apply component action callbacks. */ protected $actioncallbackapplier; /** * @var callable $visibilitycallbackapplier Function to apply component visibility callbacks. */ protected $visibilitycallbackapplier; /** * @var array Course cache for use with get_course_cached. */ protected $coursecachereference; /** * @var array Module cache reference for use with get_module_cached. */ protected $modulecachereference; /** * @var callable Bail out check for create_instance. */ protected $bailoutcheck; /** * Applies component actions to the event. * * @param event_interface $event The event to be updated. * @return event_interface The potentially modified event. */ abstract protected function apply_component_action(event_interface $event); /** * Exposes the event (or not). * * @param event_interface $event The event to potentially expose. * @return event_interface|null The exposed event or null. */ abstract protected function expose_event(event_interface $event); /** * Constructor. * * @param callable $actioncallbackapplier Function to apply component action callbacks. * @param callable $visibilitycallbackapplier Function to apply component visibility callbacks. * @param callable $bailoutcheck Function to test if we can return null early. * @param array $coursecachereference Cache to use with get_course_cached. * @param array $modulecachereference Cache to use with get_module_cached. */ public function __construct( callable $actioncallbackapplier, callable $visibilitycallbackapplier, callable $bailoutcheck, array &$coursecachereference, array &$modulecachereference ) { $this->actioncallbackapplier = $actioncallbackapplier; $this->visibilitycallbackapplier = $visibilitycallbackapplier; $this->bailoutcheck = $bailoutcheck; $this->coursecachereference = &$coursecachereference; $this->modulecachereference = &$modulecachereference; } public function create_instance(\stdClass $dbrow) { if ($dbrow->modulename && $dbrow->instance && $dbrow->courseid == 0) { // Some events (for example user overrides) may contain module instance but not course id. Find course id. $cm = calendar_get_module_cached($this->modulecachereference, $dbrow->modulename, $dbrow->instance); $dbrow->courseid = $cm->course; } $bailcheck = $this->bailoutcheck; $bail = $bailcheck($dbrow); if (!is_bool($bail)) { throw new invalid_callback_exception( 'Bail check must return true or false' ); } if ($bail) { return null; } $category = null; $course = null; $group = null; $user = null; $module = null; $subscription = null; $component = null; if ($dbrow->modulename && $dbrow->instance) { $module = new cm_info_proxy($dbrow->modulename, $dbrow->instance, $dbrow->courseid); } if ($dbrow->categoryid) { $category = new coursecat_proxy($dbrow->categoryid); } $course = new std_proxy($dbrow->courseid, function($id) { return calendar_get_course_cached($this->coursecachereference, $id); }); if ($dbrow->groupid) { $group = new std_proxy($dbrow->groupid, function($id) { return calendar_get_group_cached($id); }); } if ($dbrow->userid) { $user = new std_proxy($dbrow->userid, function($id) { global $DB; return $DB->get_record('user', ['id' => $id]); }); } if ($dbrow->subscriptionid) { $subscription = new std_proxy($dbrow->subscriptionid, function($id) { return calendar_get_subscription($id); }); } if (!empty($dbrow->repeatid)) { $repeatcollection = new repeat_event_collection($dbrow, $this); } else { $repeatcollection = null; } if (!empty($dbrow->component)) { $component = $dbrow->component; } $event = new event( $dbrow->id, $dbrow->name, new event_description($dbrow->description, $dbrow->format), $category, $course, $group, $user, $repeatcollection, $module, $dbrow->eventtype, new event_times( (new \DateTimeImmutable())->setTimestamp($dbrow->timestart), (new \DateTimeImmutable())->setTimestamp($dbrow->timestart + $dbrow->timeduration), (new \DateTimeImmutable())->setTimestamp($dbrow->timesort ? $dbrow->timesort : $dbrow->timestart), (new \DateTimeImmutable())->setTimestamp($dbrow->timemodified), (new \DateTimeImmutable())->setTimestamp($dbrow->timesort ? usergetmidnight($dbrow->timesort) : 0) ), !empty($dbrow->visible), $subscription, $dbrow->location, $component ); $isactionevent = !empty($dbrow->type) && $dbrow->type == CALENDAR_EVENT_TYPE_ACTION; return $isactionevent ? $this->expose_event($this->apply_component_action($event)) : $event; } } PK*\WnUU9classes/local/event/data_access/event_vault_interface.phpnu[. /** * Event vault interface * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\data_access; defined('MOODLE_INTERNAL') || die(); use core_calendar\local\event\entities\event_interface; /** * Interface for an event vault class * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface event_vault_interface { /** * Retrieve an event for the given id. * * @param int $id The event id * @return event_interface|false */ public function get_event_by_id($id); /** * Get all events restricted by various parameters, taking in to account user and group overrides. * * @param int|null $timestartfrom Events with timestart from this value (inclusive). * @param int|null $timestartto Events with timestart until this value (inclusive). * @param int|null $timesortfrom Events with timesort from this value (inclusive). * @param int|null $timesortto Events with timesort until this value (inclusive). * @param event_interface|null $timestartafterevent Restrict the events in the timestart range to ones after this one. * @param event_interface|null $timesortafterevent Restrict the events in the timesort range to ones after this one. * @param int $limitnum Return at most this number of events. * @param int|null $type Return only events of this type. * @param array|null $usersfilter Return only events for these users. * @param array|null $groupsfilter Return only events for these groups. * @param array|null $coursesfilter Return only events for these courses. * @param bool $withduration If true return only events starting within specified * timestart otherwise return in progress events as well. * @param bool $ignorehidden If true don't return hidden events. * @param callable|null $filter Additional logic to filter out unwanted events. * Must return true to keep the event, false to discard it. * @param string|null $searchvalue The value a user wishes to search against * @return event_interface[] Array of event_interfaces. */ public function get_events( $timestartfrom = null, $timestartto = null, $timesortfrom = null, $timesortto = null, ?event_interface $timestartafterevent = null, ?event_interface $timesortafterevent = null, $limitnum = 20, $type = null, ?array $usersfilter = null, ?array $groupsfilter = null, ?array $coursesfilter = null, ?array $categoriesfilter = null, $withduration = true, $ignorehidden = true, ?callable $filter = null, ?string $searchvalue = null ); /** * Retrieve an array of events for the given user and time constraints. * * If using this function for pagination then you can provide the last event that you've seen * ($afterevent) and it will be used to appropriately offset the result set so that you don't * receive the same events again. * @param \stdClass $user The user for whom the events belong * @param int $timesortfrom Events with timesort from this value (inclusive) * @param int $timesortto Events with timesort until this value (inclusive) * @param event_interface $afterevent Only return events after this one * @param int $limitnum Return at most this number of events * @param bool $lmittononsuspendedevents Limit course events to courses the user is active in (not suspended). * @param string|null $searchvalue The value a user wishes to search against * @return event_interface */ public function get_action_events_by_timesort( \stdClass $user, $timesortfrom, $timesortto, event_interface $afterevent, $limitnum, $limittononsuspendedevents, ?string $searchvalue = null ); /** * Retrieve an array of events for the given user filtered by the course and time constraints. * * If using this function for pagination then you can provide the last event that you've seen * ($afterevent) and it will be used to appropriately offset the result set so that you don't * receive the same events again. * * @param \stdClass $user The user for whom the events belong * @param \stdClass $course The course to filter by * @param int $timesortfrom Events with timesort from this value (inclusive) * @param int $timesortto Events with timesort until this value (inclusive) * @param event_interface $afterevent Only return events after this one * @param int $limitnum Return at most this number of events * @param string|null $searchvalue The value a user wishes to search against * @return action_event_interface */ public function get_action_events_by_course( \stdClass $user, \stdClass $course, $timesortfrom, $timesortto, event_interface $afterevent, $limitnum, ?string $searchvalue = null ); } PK*\_`=`=/classes/local/event/data_access/event_vault.phpnu[. /** * Event vault class * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\data_access; defined('MOODLE_INTERNAL') || die(); use core_calendar\local\event\entities\action_event_interface; use core_calendar\local\event\entities\event_interface; use core_calendar\local\event\exceptions\limit_invalid_parameter_exception; use core_calendar\local\event\factories\action_factory_interface; use core_calendar\local\event\factories\event_factory_interface; use core_calendar\local\event\strategies\raw_event_retrieval_strategy_interface; /** * Event vault class. * * This class will handle interacting with the database layer to retrieve * the records. This is required to house the complex logic required for * pagination because it's not a one-to-one mapping between database records * and users. * * This is a repository. It's called a vault to reduce confusion because * Moodle has already taken the name repository. Vault is cooler anyway. * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_vault implements event_vault_interface { /** * @var event_factory_interface $factory Factory for creating events. */ protected $factory; /** * @var raw_event_retrieval_strategy_interface $retrievalstrategy Strategy for getting events from the DB. */ protected $retrievalstrategy; /** * Create an event vault. * * @param event_factory_interface $factory An event factory * @param raw_event_retrieval_strategy_interface $retrievalstrategy */ public function __construct( event_factory_interface $factory, raw_event_retrieval_strategy_interface $retrievalstrategy ) { $this->factory = $factory; $this->retrievalstrategy = $retrievalstrategy; } public function get_event_by_id($id) { global $DB; if ($record = $DB->get_record('event', ['id' => $id])) { return $this->transform_from_database_record($record); } else { return false; } } public function get_events( $timestartfrom = null, $timestartto = null, $timesortfrom = null, $timesortto = null, ?event_interface $timestartafterevent = null, ?event_interface $timesortafterevent = null, $limitnum = 20, $type = null, ?array $usersfilter = null, ?array $groupsfilter = null, ?array $coursesfilter = null, ?array $categoriesfilter = null, $withduration = true, $ignorehidden = true, ?callable $filter = null, ?string $searchvalue = null ) { $fromquery = function($field, $timefrom, $lastseenmethod, $afterevent, $withduration) { if (!$timefrom) { return false; } return $this->timefield_pagination_from( $field, $timefrom, $afterevent ? $afterevent->get_times()->{$lastseenmethod}()->getTimestamp() : null, $afterevent ? $afterevent->get_id() : null, $withduration ); }; $toquery = function($field, $timeto, $lastseenmethod, $afterevent) { if (!$timeto) { return false; } return $this->timefield_pagination_to( $field, $timeto, $afterevent ? $afterevent->get_times()->{$lastseenmethod}()->getTimestamp() : null, $afterevent ? $afterevent->get_id() : null ); }; $timesortfromquery = $fromquery('timesort', $timesortfrom, 'get_sort_time', $timesortafterevent, $withduration); $timesorttoquery = $toquery('timesort', $timesortto, 'get_sort_time', $timesortafterevent); $timestartfromquery = $fromquery('timestart', $timestartfrom, 'get_start_time', $timestartafterevent, $withduration); $timestarttoquery = $toquery('timestart', $timestartto, 'get_start_time', $timestartafterevent); if (($timesortto && !$timesorttoquery) || ($timestartto && !$timestarttoquery)) { return []; } $searchquery = $this->generate_search_subquery($searchvalue); $params = array_merge( $type ? ['type' => $type] : [], $timesortfromquery ? $timesortfromquery['params'] : [], $timesorttoquery ? $timesorttoquery['params'] : [], $timestartfromquery ? $timestartfromquery['params'] : [], $timestarttoquery ? $timestarttoquery['params'] : [], $searchquery ? $searchquery['params'] : [], ); $where = array_merge( $type ? ['type = :type'] : [], $timesortfromquery ? $timesortfromquery['where'] : [], $timesorttoquery ? $timesorttoquery['where'] : [], $timestartfromquery ? $timestartfromquery['where'] : [], $timestarttoquery ? $timestarttoquery['where'] : [], $searchquery ? [$searchquery['where']] : [], ); $offset = 0; $events = []; while ($records = array_values($this->retrievalstrategy->get_raw_events( $usersfilter, $groupsfilter, $coursesfilter, $categoriesfilter, $where, $params, "COALESCE(e.timesort, e.timestart) ASC, e.id ASC", $offset, $limitnum, $ignorehidden ))) { foreach ($records as $record) { if ($event = $this->transform_from_database_record($record)) { $filtertest = $filter ? $filter($event) : true; if ($event && $filtertest) { $events[] = $event; } if (count($events) == $limitnum) { // We've got all of the events so break both loops. break 2; } } } if (!$limitnum) { break; } else { $offset += $limitnum; } } return $events; } public function get_action_events_by_timesort( \stdClass $user, $timesortfrom = null, $timesortto = null, ?event_interface $afterevent = null, $limitnum = 20, $limittononsuspendedevents = false, ?string $searchvalue = null ) { $courseids = array_map(function($course) { return $course->id; }, enrol_get_all_users_courses($user->id, $limittononsuspendedevents)); $groupids = array_reduce($courseids, function($carry, $courseid) use ($user) { $groupings = groups_get_user_groups($courseid, $user->id); // Grouping 0 is all groups. return array_merge($carry, $groupings[0]); }, []); // Always include the site events. $courseids = $courseids ? array_merge($courseids, [SITEID]) : $courseids; return $this->get_events( null, null, $timesortfrom, $timesortto, null, $afterevent, $limitnum, CALENDAR_EVENT_TYPE_ACTION, [$user->id], $groupids ? $groupids : null, $courseids ? $courseids : null, null, // All categories. true, true, function ($event) { return $event instanceof action_event_interface; }, $searchvalue ); } public function get_action_events_by_course( \stdClass $user, \stdClass $course, $timesortfrom = null, $timesortto = null, ?event_interface $afterevent = null, $limitnum = 20, ?string $searchvalue = null ) { $groupings = groups_get_user_groups($course->id, $user->id); return array_values( $this->get_events( null, null, $timesortfrom, $timesortto, null, $afterevent, $limitnum, CALENDAR_EVENT_TYPE_ACTION, [$user->id], $groupings[0] ? $groupings[0] : null, [$course->id], [], true, true, function ($event) use ($course) { return $event instanceof action_event_interface && $event->get_course()->get('id') == $course->id; }, $searchvalue ) ); } /** * Generates SQL subquery and parameters for 'from' pagination. * * @param string $field * @param int $timefrom * @param int|null $lastseentime * @param int|null $lastseenid * @param bool $withduration * @return array */ protected function timefield_pagination_from( $field, $timefrom, $lastseentime = null, $lastseenid = null, $withduration = true ) { $where = ''; $params = []; if ($lastseentime && $lastseentime >= $timefrom) { $where = '((timesort = :timefrom1 AND e.id > :timefromid) OR timesort > :timefrom2)'; if ($field === 'timestart') { $where = '((timestart = :timefrom1 AND e.id > :timefromid) OR timestart > :timefrom2' . ($withduration ? ' OR timestart + timeduration > :timefrom3' : '') . ')'; } $params['timefromid'] = $lastseenid; $params['timefrom1'] = $lastseentime; $params['timefrom2'] = $lastseentime; $params['timefrom3'] = $lastseentime; } else { $where = 'timesort >= :timefrom'; if ($field === 'timestart') { $where = '(timestart >= :timefrom' . ($withduration ? ' OR timestart + timeduration > :timefrom2' : '') . ')'; } $params['timefrom'] = $timefrom; $params['timefrom2'] = $timefrom; } return ['where' => [$where], 'params' => $params]; } /** * Generates SQL subquery and parameters for 'to' pagination. * * @param string $field * @param int $timeto * @param int|null $lastseentime * @param int|null $lastseenid * @return array|bool */ protected function timefield_pagination_to( $field, $timeto, $lastseentime = null, $lastseenid = null ) { $where = []; $params = []; if ($lastseentime && $lastseentime > $timeto) { // The last seen event from this set is after the time sort range which // means all events in this range have been seen, so we can just return // early here. return false; } else if ($lastseentime && $lastseentime == $timeto) { $where[] = '((timesort = :timeto1 AND e.id > :timetoid) OR timesort < :timeto2)'; if ($field === 'timestart') { $where[] = '((timestart = :timeto1 AND e.id > :timetoid) OR timestart < :timeto2)'; } $params['timetoid'] = $lastseenid; $params['timeto1'] = $timeto; $params['timeto2'] = $timeto; } else { $where[] = ($field === 'timestart' ? 'timestart' : 'timesort') . ' <= :timeto'; $params['timeto'] = $timeto; } return ['where' => $where, 'params' => $params]; } /** * Create an event from a database record. * * @param \stdClass $record The database record * @return event_interface|null */ protected function transform_from_database_record(\stdClass $record) { return $this->factory->create_instance($record); } /** * Fetches records from DB. * * @param int $userid * @param array|null $whereconditions * @param array $whereparams * @param string $ordersql * @param int $offset * @param int $limitnum * @return array */ protected function get_from_db( $userid, $whereconditions, $whereparams, $ordersql, $offset, $limitnum ) { return array_values( $this->retrievalstrategy->get_raw_events( [$userid], null, null, null, $whereconditions, $whereparams, $ordersql, $offset, $limitnum ) ); } /** * Generates SQL subquery and parameters for event searching. * * @param string|null $searchvalue Search value. * @return array|null */ protected function generate_search_subquery(?string $searchvalue): ?array { global $CFG, $DB; if (!$searchvalue) { return null; } $parts = preg_split('/\s+/', $searchvalue); $wherecoursenameconditions = []; $whereactivitynameconditions = []; foreach ($parts as $index => $part) { // Course name searching. $wherecoursenameconditions[] = $DB->sql_like('c.fullname', ':cfullname' . $index, false); $params['cfullname'. $index] = '%' . $DB->sql_like_escape($part) . '%'; // Activity name searching. $whereactivitynameconditions[] = $DB->sql_like('e.name', ':eventname' . $index, false); $params['eventname'. $index] = '%' . $DB->sql_like_escape($part) . '%'; } // Activity type searching. $whereconditions[] = $DB->sql_like('e.modulename', ':modulename', false); $params['modulename'] = '%' . $DB->sql_like_escape($searchvalue) . '%'; // Activity type searching (localised type name). require_once($CFG->dirroot . '/course/lib.php'); // Search in modules' singular and plural names. $modules = array_keys(array_merge( preg_grep('/' . $searchvalue . '/i', get_module_types_names()) ?: [], preg_grep('/' . $searchvalue . '/i', get_module_types_names(true)) ?: [], )); if ($modules) { [$insql, $inparams] = $DB->get_in_or_equal($modules, SQL_PARAMS_NAMED, 'exactmodulename'); $whereconditions[] = 'e.modulename ' . $insql; $params += $inparams; } $whereclause = '('; $whereclause .= implode(' OR ', $whereconditions); $whereclause .= ' OR (' . implode(' AND ', $wherecoursenameconditions) . ')'; $whereclause .= ' OR (' . implode(' AND ', $whereactivitynameconditions) . ')'; $whereclause .= ')'; return ['where' => $whereclause, 'params' => $params]; } } PK*\vBclasses/local/event/exceptions/member_does_not_exist_exception.phpnu[. /** * Member does not exist exception. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\exceptions; defined('MOODLE_INTERNAL') || die(); /** * Member does not exist exception. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class member_does_not_exist_exception extends \moodle_exception { } PK*\>classes/local/event/exceptions/invalid_parameter_exception.phpnu[. /** * General invalid parameter exception. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\exceptions; defined('MOODLE_INTERNAL') || die(); /** * General invalid parameter exception. * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class invalid_parameter_exception extends \moodle_exception { } PK*\qDclasses/local/event/exceptions/limit_invalid_parameter_exception.phpnu[. /** * Invalid limit parameter exception. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\exceptions; defined('MOODLE_INTERNAL') || die(); /** * Invalid limit parameter exception. * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class limit_invalid_parameter_exception extends invalid_parameter_exception { } PK*\Iݑ=classes/local/event/exceptions/invalid_callback_exception.phpnu[. /** * Invalid callback exception. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\exceptions; defined('MOODLE_INTERNAL') || die(); /** * Invalid callback exception. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class invalid_callback_exception extends \moodle_exception { } PK*\zGclasses/local/event/exceptions/timesort_invalid_parameter_exception.phpnu[. /** * Invalid timesort parameter exception. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event\exceptions; defined('MOODLE_INTERNAL') || die(); /** * Invalid timesort parameter exception. * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class timesort_invalid_parameter_exception extends invalid_parameter_exception { } PK*\AJ classes/type_base.phpnu[. namespace core_calendar; /** * Defines functions used by calendar type plugins. * * This library provides a unified interface for calendar types. * * @package core_calendar * @copyright 2008 onwards Foodle Group {@link http://foodle.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class type_base { /** * Returns the name of the calendar. * * This is the non-translated name, usually just * the name of the calendar folder. * * @return string the calendar name */ abstract public function get_name(); /** * Returns a list of all the possible days for all months. * * This is used to generate the select box for the days * in the date selector elements. Some months contain more days * than others so this function should return all possible days as * we can not predict what month will be chosen (the user * may have JS turned off and we need to support this situation in * Moodle). * * @return array the days */ abstract public function get_days(); /** * Returns a list of all the names of the months. * * @return array the month names */ abstract public function get_months(); /** * Returns the minimum year for the calendar. * * @return int The minimum year */ abstract public function get_min_year(); /** * Returns the maximum year for the calendar * * @return int The maximum year */ abstract public function get_max_year(); /** * Returns an array of years. * * @param int $minyear * @param int $maxyear * @return array the years */ abstract public function get_years($minyear = null, $maxyear = null); /** * Returns a multidimensional array with information for day, month, year * and the order they are displayed when selecting a date. * The order in the array will be the order displayed when selecting a date. * Override this function to change the date selector order. * * @param int $minyear The year to start with * @param int $maxyear The year to finish with * @return array Full date information */ abstract public function get_date_order($minyear = null, $maxyear = null); /** * Returns the number of days in a week. * * @return int the number of days */ abstract public function get_num_weekdays(); /** * Returns an indexed list of all the names of the weekdays. * * The list starts with the index 0. Each index, representing a * day, must be an array that contains the indexes 'shortname' * and 'fullname'. * * @return array array of days */ abstract public function get_weekdays(); /** * Returns the index of the starting week day. * * This may vary, for example in the Gregorian calendar, some may consider Monday * as the start of the week, where as others may consider Sunday the start. * * @return int */ abstract public function get_starting_weekday(); /** * Returns the index of the weekday for a specific calendar date. * * @param int $year * @param int $month * @param int $day * @return int */ abstract public function get_weekday($year, $month, $day); /** * Returns the number of days in a given month. * * * @param int $year * @param int $month * @return int the number of days */ abstract public function get_num_days_in_month($year, $month); /** * Get the previous month. * * @param int $year * @param int $month * @return array previous month and year */ abstract public function get_prev_month($year, $month); /** * Get the next month. * * @param int $year * @param int $month * @return array the following month and year */ abstract public function get_next_month($year, $month); /** * Returns a formatted string that represents a date in user time. * * @param int $time the timestamp in UTC, as obtained from the database * @param string $format strftime format * @param int|float|string $timezone the timezone to use * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} * @param bool $fixday if true then the leading zero from %d is removed, * if false then the leading zero is maintained * @param bool $fixhour if true then the leading zero from %I is removed, * if false then the leading zero is maintained * @return string the formatted date/time */ abstract public function timestamp_to_date_string($time, $format, $timezone, $fixday, $fixhour); /** * Given a $time timestamp in GMT (seconds since epoch), returns an array that represents * the date in user time. * * @param int $time timestamp in GMT * @param float|int|string $timezone the timezone to use to calculate the time * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} * @return array an array that represents the date in user time */ abstract public function timestamp_to_date_array($time, $timezone = 99); /** * Provided with a day, month, year, hour and minute in the specific * calendar type convert it into the equivalent Gregorian date. * * @param int $year * @param int $month * @param int $day * @param int $hour * @param int $minute * @return array the converted date */ abstract public function convert_to_gregorian($year, $month, $day, $hour = 0, $minute = 0); /** * Provided with a day, month, year, hour and minute in a Gregorian date * convert it into the specific calendar type date. * * @param int $year * @param int $month * @param int $day * @param int $hour * @param int $minute * @return array the converted date */ abstract public function convert_from_gregorian($year, $month, $day, $hour = 0, $minute = 0); /** * This return locale for windows os. * * @return string locale */ abstract public function locale_win_charset(); /** * Provided with a day, month, year, hour and minute in the specific * calendar type convert it into the equivalent Unix Time Stamp. * * @param int $year * @param int $month * @param int $day * @param int $hour * @param int $minute * @return int timestamp */ public function convert_to_timestamp($year, $month, $day, $hour = 0, $minute = 0) { $gregorianinfo = $this->convert_to_gregorian($year, $month, $day, $hour, $minute); return make_timestamp( $gregorianinfo['year'], $gregorianinfo['month'], $gregorianinfo['day'], $gregorianinfo['hour'], $gregorianinfo['minute'], 0); } /** * Get the previous day. * * @param int $daytimestamp The day timestamp. * @return int previous day timestamp */ public function get_prev_day($daytimestamp) { $date = new \DateTime(); $date->setTimestamp($daytimestamp); $date->modify('-1 day'); return $date->getTimestamp(); } /** * Get the next day. * * @param int $daytimestamp The day timestamp. * @return int the following day */ public function get_next_day($daytimestamp) { $date = new \DateTime(); $date->setTimestamp($daytimestamp); $date->modify('+1 day'); return $date->getTimestamp(); } } PK*\==,classes/output/refreshintervalcollection.phpnu[. /** * Class to display collection select for the refresh interval. * * @package core_calendar * @copyright 2021 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\output; use core\output\inplace_editable; class refreshintervalcollection extends inplace_editable { /** * Constructor. * * @param \stdClass $subscription Subscription object */ public function __construct(\stdClass $subscription) { $collection = calendar_get_pollinterval_choices(); parent::__construct('core_calendar', 'refreshinterval', $subscription->id, true, null, $subscription->pollinterval, null, get_string('pollinterval', 'calendar')); $this->set_type_select($collection); } public static function update(int $subscriptionid, int $pollinterval) { if (calendar_can_edit_subscription($subscriptionid)) { $subscription = calendar_get_subscription($subscriptionid); $subscription->pollinterval = $pollinterval; calendar_update_subscription($subscription); $tmpl = new self($subscription); return $tmpl; } else { throw new \moodle_exception('nopermissions', 'error', '', get_string('managesubscriptions', 'calendar')); } } } PK*\6kz!!"classes/output/humantimeperiod.phpnu[. namespace core_calendar\output; Use DateTimeInterface; use DateTimeImmutable; use core\output\templatable; use core\output\renderable; use core\output\renderer_base; use core\url; /** * Class humantimeperiod. * * This class is used to render a time period as a human readable date. * The main difference between userdate and this class is that this class * will render the date as "Today", "Yesterday", "Tomorrow" if the date is * close to the current date. Also, it will add styling if the date * is near. * * @package core_calendar * @copyright 2025 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class humantimeperiod implements renderable, templatable { /** @var int|null Number of seconds that indicates a nearby date. Default to DAYSECS. Use null for no indication. */ protected $near = DAYSECS; /** @var url|null URL to link the date to. */ protected ?url $link = null; /** @var string|null An optional date format to apply. */ protected ?string $langtimeformat = null; /** @var bool Whether to use human common words or not. */ protected bool $userelatives = true; /** * Class constructor. * * @param DateTimeImmutable $startdatetime The starting timestamp. * @param DateTimeImmutable|null $enddatetime The ending timestamp. */ protected function __construct( /** @var DateTimeImmutable $startdatetime The starting date time. */ protected DateTimeImmutable $startdatetime, /** @var DateTimeImmutable|null $enddatetime The ending date time. */ protected ?DateTimeImmutable $enddatetime, ) { } /** * Creates a new humantimeperiod instance from a timestamp. * * @param int $starttimestamp The starting timestamp. * @param int|null $endtimestamp The ending timestamp. * @param int|null $near The number of seconds that indicates a nearby date. Default to DAYSECS, use null for no indication. * @param url|null $link URL to link the date to. * @param string|null $langtimeformat Lang date and time format to use to format the date. * @param bool $userelatives Whether to use human common words or not. * @return humantimeperiod The new instance. */ public static function create_from_timestamp( int $starttimestamp, ?int $endtimestamp, ?int $near = DAYSECS, ?url $link = null, ?string $langtimeformat = null, bool $userelatives = true, ): self { return self::create_from_datetime( (new DateTimeImmutable("@{$starttimestamp}")), $endtimestamp ? (new DateTimeImmutable("@{$endtimestamp}")) : null, $near, $link, $langtimeformat, $userelatives ); } /** * Creates a new humantimeperiod instance from a datetime. * * @param DateTimeInterface $startdatetime The starting datetime. * @param DateTimeInterface|null $enddatetime The ending datetime. * @param int|null $near The number of seconds that indicates a nearby date. Default to DAYSECS, use null for no indication. * @param url|null $link URL to link the date to. * @param string|null $langtimeformat Lang date and time format to use to format the date. * @param bool $userelatives Whether to use human common words or not. * @return humantimeperiod The new instance. */ public static function create_from_datetime( DateTimeInterface $startdatetime, ?DateTimeInterface $enddatetime, ?int $near = DAYSECS, ?url $link = null, ?string $langtimeformat = null, bool $userelatives = true, ): self { // Always use an Immutable object to ensure that the value does not change externally before it is rendered. if (!($startdatetime instanceof DateTimeImmutable)) { $startdatetime = DateTimeImmutable::createFromInterface($startdatetime); } if ($enddatetime != null && !($enddatetime instanceof DateTimeImmutable)) { $enddatetime = DateTimeImmutable::createFromInterface($enddatetime); } return (new self($startdatetime, $enddatetime)) ->set_near_limit($near) ->set_link($link) ->set_lang_time_format($langtimeformat) ->set_use_relatives($userelatives); } /** * Sets the number of seconds within which a date is considered near. * * @param int|null $near The number of seconds within which a date is considered near. * @return humantimeperiod The instance. */ public function set_near_limit(?int $near): self { $this->near = $near; return $this; } /** * Sets the link for the date. If null, no link will be added. * * @param url|null $link The link for the date. * @return humantimeperiod The instance. */ public function set_link(?url $link): self { $this->link = $link; return $this; } /** * Sets an optional date format to apply. * * @param string|null $langtimeformat Lang date and time format to use to format the date. * @return humantimeperiod The instance. */ public function set_lang_time_format(?string $langtimeformat): self { $this->langtimeformat = $langtimeformat; return $this; } /** * Sets whether to use human relative terminology. * * @param bool $userelatives Whether to use human relative terminology. * @return humantimeperiod The instance. */ public function set_use_relatives(bool $userelatives): self { $this->userelatives = $userelatives; return $this; } #[\Override] public function export_for_template(renderer_base $output): array { $period = $this->format_period(); return [ 'startdate' => $period['startdate']->export_for_template($output), 'enddate' => $period['enddate'] ? $period['enddate']->export_for_template($output) : null, ]; } /** * Format a time periods based on 2 dates. * * @return array An array of one or two humandate elements. */ private function format_period(): array { $linkstart = null; $linkend = null; if ($this->link) { $linkstart = new url($this->link, ['view' => 'day', 'time' => $this->startdatetime->getTimestamp()]); $linkend = new url($this->link, ['view' => 'day', 'time' => $this->enddatetime->getTimestamp()]); } $startdate = humandate::create_from_datetime( datetime: $this->startdatetime, near: $this->near, link: $linkstart, langtimeformat: $this->langtimeformat, userelatives: $this->userelatives ); if ($this->enddatetime == null || $this->startdatetime == $this->enddatetime) { return [ 'startdate' => $startdate, 'enddate' => null, ]; } // Get the midnight of the day the event will start. $usermidnightstart = usergetmidnight($this->startdatetime->getTimestamp()); // Get the midnight of the day the event will end. $usermidnightend = usergetmidnight($this->enddatetime->getTimestamp()); // Check if we will still be on the same day. $issameday = ($usermidnightstart == $usermidnightend); $enddate = humandate::create_from_datetime( datetime: $this->enddatetime, near: $this->near, timeonly: $issameday, link: $linkend, langtimeformat: $this->langtimeformat, userelatives: $this->userelatives ); return [ 'startdate' => $startdate, 'enddate' => $enddate, ]; } } PK*\Ó22classes/output/humandate.phpnu[. namespace core_calendar\output; use DateInterval; use DateTimeInterface; use DateTimeImmutable; use core\output\pix_icon; use core\output\templatable; use core\output\renderable; use core\output\renderer_base; use core\clock; use core\url; /** * Class humandate. * * This class is used to render a timestamp as a human readable date. * The main difference between userdate and this class is that this class * will render the date as "Today", "Yesterday", "Tomorrow" if the date is * close to the current date. Also, it will add alert styling if the date * is near. * * @package core_calendar * @copyright 2024 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class humandate implements renderable, templatable { /** @var int|null The number of seconds within which a date is considered near. 1 day by default. */ protected ?int $near = DAYSECS; /** @var bool Whether we should show time only or date and time. */ protected bool $timeonly = false; /** @var url|null Link for the date. */ protected ?url $link = null; /** @var string|null An optional date format to apply. */ protected ?string $langtimeformat = null; /** @var bool Whether to use human relative terminology. */ protected bool $userelatives = true; /** @var clock The clock interface to handle time. */ protected clock $clock; /** * Class constructor. * * Use the factory methods, such as create_from_timestamp or create_from_datetime, instead. * * @param DateTimeImmutable $datetime The datetime. */ protected function __construct( /** @var DateTimeImmutable $datetime The datetime. **/ protected DateTimeImmutable $datetime, ) { $this->clock = \core\di::get(clock::class); } /** * Creates a new humandate instance from a timestamp. * * @param int $timestamp The timestamp. * @param int|null $near The number of seconds within which a date is considered near. 1 day by default. * @param bool $timeonly Whether we should show time only or date and time. * @param url|null $link Link for the date. * @param string|null $langtimeformat An optional date format to apply. * @param bool $userelatives Whether to use human relative terminology. * @return humandate The new instance. */ public static function create_from_timestamp( int $timestamp, ?int $near = DAYSECS, bool $timeonly = false, ?url $link = null, ?string $langtimeformat = null, bool $userelatives = true, ): self { return self::create_from_datetime( (new DateTimeImmutable("@{$timestamp}")), $near, $timeonly, $link, $langtimeformat, $userelatives ); } /** * Creates a new humandate instance from a datetime. * * @param DateTimeInterface $datetime The datetime. * @param int|null $near The number of seconds within which a date is considered near. 1 day by default. * @param bool $timeonly Whether we should show time only or date and time. * @param url|null $link Link for the date. * @param string|null $langtimeformat An optional date format to apply. * @param bool $userelatives Whether to use human relative terminology. * @return humandate The new instance. */ public static function create_from_datetime( DateTimeInterface $datetime, ?int $near = DAYSECS, bool $timeonly = false, ?url $link = null, ?string $langtimeformat = null, bool $userelatives = true, ): self { if (!($datetime instanceof DateTimeImmutable)) { // Always use an Immutable object to ensure that the value does not change externally before it is rendered. $datetime = DateTimeImmutable::createFromInterface($datetime); } return (new self($datetime)) ->set_near_limit($near) ->set_display_time_only($timeonly) ->set_link($link) ->set_lang_time_format($langtimeformat) ->set_use_relatives($userelatives); } /** * Sets the number of seconds within which a date is considered near. * * @param int|null $near The number of seconds within which a date is considered near. * @return humandate The instance. */ public function set_near_limit(?int $near): self { $this->near = $near; return $this; } /** * Sets whether we should show time only or date and time. * * @param bool $timeonly Whether we should show time only or date and time. * @return humandate The instance. */ public function set_display_time_only(bool $timeonly): self { $this->timeonly = $timeonly; return $this; } /** * Sets the link for the date. If null, no link will be added. * * @param url|null $link The link for the date. * @return humandate The instance. */ public function set_link(?url $link): self { $this->link = $link; return $this; } /** * Sets an optional date format to apply. * * @param string|null $langtimeformat Lang date and time format to use to format the date. * @return humandate The instance. */ public function set_lang_time_format(?string $langtimeformat): self { $this->langtimeformat = $langtimeformat; return $this; } /** * Sets whether to use human relative terminology. * * @param bool $userelatives Whether to use human relative terminology. * @return humandate The instance. */ public function set_use_relatives(bool $userelatives): self { $this->userelatives = $userelatives; return $this; } #[\Override] public function export_for_template(renderer_base $output): array { $userdate = $this->default_userdate(); $relative = null; if ($this->userelatives) { $relative = $this->format_relative_date(); } if ($this->timeonly) { $date = null; } else { $date = $relative ?? $userdate; } $data = [ 'timestamp' => $this->datetime->getTimestamp(), 'userdate' => $userdate, 'date' => $date, 'time' => $this->format_time(), 'ispast' => $this->datetime < $this->clock->now(), 'needtitle' => ($relative !== null || $this->timeonly), 'link' => $this->link ? $this->link->out(false) : '', ]; if ($this->is_near()) { $icon = new pix_icon( pix: 'i/warning', alt: get_string('warning'), component: 'moodle', attributes: ['class' => 'me-0 pb-1'] ); $data['isnear'] = true; $data['nearicon'] = $icon->export_for_template($output); } return $data; } /** * Returns the default user date format. * * @return string The formatted date. */ private function default_userdate(): string { $timestamp = $this->datetime->getTimestamp(); if ($this->is_current_year()) { $format = get_string('strftimedayshort', 'langconfig'); } else { $format = get_string('strftimedaydate', 'langconfig'); } return userdate($timestamp, $format); } /** * Checks if the date is near. * * @return bool Whether the date is near. */ private function is_near(): bool { if ($this->near === null) { return false; } $due = $this->datetime->diff($this->clock->now()); $intervalseconds = $this->interval_to_seconds($due); return $intervalseconds < $this->near && $intervalseconds > 0; } /** * Checks if the datetime is from the current year. * * @return bool True if the datetime is from the current year, false otherwise. */ private function is_current_year(): bool { $currentyear = $this->clock->now()->format('Y'); $datetimeyear = $this->datetime->format('Y'); return $currentyear === $datetimeyear; } /** * Converts a DateInterval object to total seconds. * * @param \DateInterval $interval The interval to convert. * @return int The total number of seconds. */ private function interval_to_seconds(DateInterval $interval): int { $reference = new DateTimeImmutable(); $entime = $reference->add($interval); return $reference->getTimestamp() - $entime->getTimestamp(); } /** * Formats the timestamp as a relative date string (e.g., "Today", "Yesterday", "Tomorrow"). * * This method compares the given timestamp with the current date and returns a formatted * string representing the relative date. If the timestamp corresponds to today, yesterday, * or tomorrow, it returns the appropriate string. Otherwise, it returns null. * * @return string|null */ private function format_relative_date(): ?string { $usertimestamp = $this->get_user_date($this->datetime->getTimestamp()); if ($usertimestamp == $this->get_user_date($this->clock->now()->getTimestamp())) { $format = get_string( 'timerelativetoday', 'calendar', get_string('strftimedateshort', 'langconfig') ); } else if ($usertimestamp == $this->get_user_date(strtotime('yesterday', $this->clock->now()->getTimestamp()))) { $format = get_string( 'timerelativeyesterday', 'calendar', get_string('strftimedateshort', 'langconfig') ); } else if ($usertimestamp == $this->get_user_date(strtotime('tomorrow', $this->clock->now()->getTimestamp()))) { $format = get_string( 'timerelativetomorrow', 'calendar', get_string('strftimedateshort', 'langconfig') ); } else { return null; } return userdate($this->datetime->getTimestamp(), $format); } /** * Formats the timestamp as a human readable time. * * @param int $timestamp The timestamp to format. * @param string $format The format to use. * @return string The formatted date. */ private function get_user_date(int $timestamp, string $format = '%Y-%m-%d'): string { $calendartype = \core_calendar\type_factory::get_calendar_instance(); $timezone = \core_date::get_user_timezone_object(); return $calendartype->timestamp_to_date_string( time: $timestamp, format: $format, timezone: $timezone->getName(), fixday: true, fixhour: true, ); } /** * Formats the timestamp as a human readable time. * * This method compares the given timestamp with the current date and returns a formatted * string representing the time. * * @return string */ private function format_time(): string { global $CFG; // Ensure calendar constants are loaded. require_once($CFG->dirroot . '/calendar/lib.php'); $timeformat = get_user_preferences('calendar_timeformat'); if (empty($timeformat)) { $timeformat = get_config(null, 'calendar_site_timeformat'); } // Allow language customization of selected time format. if ($timeformat === CALENDAR_TF_12) { $timeformat = get_string('strftimetime12', 'langconfig'); } else if ($timeformat === CALENDAR_TF_24) { $timeformat = get_string('strftimetime24', 'langconfig'); } if ($timeformat) { return userdate($this->datetime->getTimestamp(), $timeformat); } // Let's use default format. if ($this->langtimeformat === null) { $langtimeformat = get_string('strftimetime'); } return userdate($this->datetime->getTimestamp(), $langtimeformat); } } PK*\bclasses/rrule_manager.phpnu[. /** * Defines calendar class to manage recurrence rule (rrule) during ical imports. * * @package core_calendar * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar; use calendar_event; use DateInterval; use DateTime; use moodle_exception; use stdClass; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/calendar/lib.php'); /** * Defines calendar class to manage recurrence rule (rrule) during ical imports. * * Please refer to RFC 2445 {@link http://www.ietf.org/rfc/rfc2445.txt} for detail explanation of the logic. * Here is a basic extract from it to explain various params:- * recur = "FREQ"=freq *( * ; either UNTIL or COUNT may appear in a 'recur', * ; but UNTIL and COUNT MUST NOT occur in the same 'recur' * ( ";" "UNTIL" "=" enddate ) / * ( ";" "COUNT" "=" 1*DIGIT ) / * ; the rest of these keywords are optional, * ; but MUST NOT occur more than once * ( ";" "INTERVAL" "=" 1*DIGIT ) / * ( ";" "BYSECOND" "=" byseclist ) / * ( ";" "BYMINUTE" "=" byminlist ) / * ( ";" "BYHOUR" "=" byhrlist ) / * ( ";" "BYDAY" "=" bywdaylist ) / * ( ";" "BYMONTHDAY" "=" bymodaylist ) / * ( ";" "BYYEARDAY" "=" byyrdaylist ) / * ( ";" "BYWEEKNO" "=" bywknolist ) / * ( ";" "BYMONTH" "=" bymolist ) / * ( ";" "BYSETPOS" "=" bysplist ) / * ( ";" "WKST" "=" weekday ) / * ( ";" x-name "=" text ) * ) * * freq = "SECONDLY" / "MINUTELY" / "HOURLY" / "DAILY" * / "WEEKLY" / "MONTHLY" / "YEARLY" * enddate = date * enddate =/ date-time ;An UTC value * byseclist = seconds / ( seconds *("," seconds) ) * seconds = 1DIGIT / 2DIGIT ;0 to 59 * byminlist = minutes / ( minutes *("," minutes) ) * minutes = 1DIGIT / 2DIGIT ;0 to 59 * byhrlist = hour / ( hour *("," hour) ) * hour = 1DIGIT / 2DIGIT ;0 to 23 * bywdaylist = weekdaynum / ( weekdaynum *("," weekdaynum) ) * weekdaynum = [([plus] ordwk / minus ordwk)] weekday * plus = "+" * minus = "-" * ordwk = 1DIGIT / 2DIGIT ;1 to 53 * weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA" * ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, * ;FRIDAY, SATURDAY and SUNDAY days of the week. * bymodaylist = monthdaynum / ( monthdaynum *("," monthdaynum) ) * monthdaynum = ([plus] ordmoday) / (minus ordmoday) * ordmoday = 1DIGIT / 2DIGIT ;1 to 31 * byyrdaylist = yeardaynum / ( yeardaynum *("," yeardaynum) ) * yeardaynum = ([plus] ordyrday) / (minus ordyrday) * ordyrday = 1DIGIT / 2DIGIT / 3DIGIT ;1 to 366 * bywknolist = weeknum / ( weeknum *("," weeknum) ) * weeknum = ([plus] ordwk) / (minus ordwk) * bymolist = monthnum / ( monthnum *("," monthnum) ) * monthnum = 1DIGIT / 2DIGIT ;1 to 12 * bysplist = setposday / ( setposday *("," setposday) ) * setposday = yeardaynum * * @package core_calendar * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class rrule_manager { /** const string Frequency constant */ const FREQ_YEARLY = 'yearly'; /** const string Frequency constant */ const FREQ_MONTHLY = 'monthly'; /** const string Frequency constant */ const FREQ_WEEKLY = 'weekly'; /** const string Frequency constant */ const FREQ_DAILY = 'daily'; /** const string Frequency constant */ const FREQ_HOURLY = 'hourly'; /** const string Frequency constant */ const FREQ_MINUTELY = 'everyminute'; /** const string Frequency constant */ const FREQ_SECONDLY = 'everysecond'; /** const string Day constant */ const DAY_MONDAY = 'Monday'; /** const string Day constant */ const DAY_TUESDAY = 'Tuesday'; /** const string Day constant */ const DAY_WEDNESDAY = 'Wednesday'; /** const string Day constant */ const DAY_THURSDAY = 'Thursday'; /** const string Day constant */ const DAY_FRIDAY = 'Friday'; /** const string Day constant */ const DAY_SATURDAY = 'Saturday'; /** const string Day constant */ const DAY_SUNDAY = 'Sunday'; /** const int For forever repeating events, repeat for this many years */ const TIME_UNLIMITED_YEARS = 10; /** const array Array of days in a week. */ const DAYS_OF_WEEK = [ 'MO' => self::DAY_MONDAY, 'TU' => self::DAY_TUESDAY, 'WE' => self::DAY_WEDNESDAY, 'TH' => self::DAY_THURSDAY, 'FR' => self::DAY_FRIDAY, 'SA' => self::DAY_SATURDAY, 'SU' => self::DAY_SUNDAY, ]; /** @var string string representing the recurrence rule */ protected $rrule; /** @var string Frequency of event */ protected $freq; /** @var int defines a timestamp value which bounds the recurrence rule in an inclusive manner.*/ protected $until = 0; /** @var int Defines the number of occurrences at which to range-bound the recurrence */ protected $count = 0; /** @var int This rule part contains a positive integer representing how often the recurrence rule repeats */ protected $interval = 1; /** @var array List of second rules */ protected $bysecond = array(); /** @var array List of Minute rules */ protected $byminute = array(); /** @var array List of hour rules */ protected $byhour = array(); /** @var array List of day rules */ protected $byday = array(); /** @var array List of monthday rules */ protected $bymonthday = array(); /** @var array List of yearday rules */ protected $byyearday = array(); /** @var array List of weekno rules */ protected $byweekno = array(); /** @var array List of month rules */ protected $bymonth = array(); /** @var array List of setpos rules */ protected $bysetpos = array(); /** @var string Week start rule. Default is Monday. */ protected $wkst = self::DAY_MONDAY; /** * Constructor for the class * * @param string $rrule Recurrence rule */ public function __construct($rrule) { $this->rrule = $rrule; } /** * Parse the recurrence rule and setup all properties. */ public function parse_rrule() { $rules = explode(';', $this->rrule); if (empty($rules)) { return; } foreach ($rules as $rule) { $this->parse_rrule_property($rule); } // Validate the rules as a whole. $this->validate_rules(); } /** * Create events for specified rrule. * * @param calendar_event $passedevent Properties of event to create. * @throws moodle_exception */ public function create_events($passedevent) { global $DB; $event = clone($passedevent); // If Frequency is not set, there is nothing to do. if (empty($this->freq)) { return; } // Delete all child events in case of an update. This should be faster than verifying if the event exists and updating it. $where = "repeatid = ? AND id != ?"; $DB->delete_records_select('event', $where, array($event->id, $event->id)); $eventrec = $event->properties(); // Generate timestamps that obey the rrule. $eventtimes = $this->generate_recurring_event_times($eventrec); // Update the parent event. Make sure that its repeat ID is the same as its ID. $calevent = new calendar_event($eventrec); $updatedata = new stdClass(); $updatedata->repeatid = $event->id; // Also, adjust the parent event's timestart, if necessary. if (count($eventtimes) > 0 && !in_array($eventrec->timestart, $eventtimes)) { $updatedata->timestart = reset($eventtimes); } $calevent->update($updatedata, false); $eventrec->timestart = $calevent->timestart; // Create the recurring calendar events. $this->create_recurring_events($eventrec, $eventtimes); } /** * Parse a property of the recurrence rule. * * @param string $prop property string with type-value pair * @throws moodle_exception */ protected function parse_rrule_property($prop) { list($property, $value) = explode('=', $prop); switch ($property) { case 'FREQ' : $this->set_frequency($value); break; case 'UNTIL' : $this->set_until($value); break; CASE 'COUNT' : $this->set_count($value); break; CASE 'INTERVAL' : $this->set_interval($value); break; CASE 'BYSECOND' : $this->set_bysecond($value); break; CASE 'BYMINUTE' : $this->set_byminute($value); break; CASE 'BYHOUR' : $this->set_byhour($value); break; CASE 'BYDAY' : $this->set_byday($value); break; CASE 'BYMONTHDAY' : $this->set_bymonthday($value); break; CASE 'BYYEARDAY' : $this->set_byyearday($value); break; CASE 'BYWEEKNO' : $this->set_byweekno($value); break; CASE 'BYMONTH' : $this->set_bymonth($value); break; CASE 'BYSETPOS' : $this->set_bysetpos($value); break; CASE 'WKST' : $this->wkst = $this->get_day($value); break; default: // We should never get here, something is very wrong. throw new moodle_exception('errorrrule', 'calendar'); } } /** * Sets Frequency property. * * @param string $freq Frequency of event * @throws moodle_exception */ protected function set_frequency($freq) { switch ($freq) { case 'YEARLY': $this->freq = self::FREQ_YEARLY; break; case 'MONTHLY': $this->freq = self::FREQ_MONTHLY; break; case 'WEEKLY': $this->freq = self::FREQ_WEEKLY; break; case 'DAILY': $this->freq = self::FREQ_DAILY; break; case 'HOURLY': $this->freq = self::FREQ_HOURLY; break; case 'MINUTELY': $this->freq = self::FREQ_MINUTELY; break; case 'SECONDLY': $this->freq = self::FREQ_SECONDLY; break; default: // We should never get here, something is very wrong. throw new moodle_exception('errorrrulefreq', 'calendar'); } } /** * Gets the day from day string. * * @param string $daystring Day string (MO, TU, etc) * @throws moodle_exception * * @return string Day represented by the parameter. */ protected function get_day($daystring) { switch ($daystring) { case 'MO': return self::DAY_MONDAY; break; case 'TU': return self::DAY_TUESDAY; break; case 'WE': return self::DAY_WEDNESDAY; break; case 'TH': return self::DAY_THURSDAY; break; case 'FR': return self::DAY_FRIDAY; break; case 'SA': return self::DAY_SATURDAY; break; case 'SU': return self::DAY_SUNDAY; break; default: // We should never get here, something is very wrong. throw new moodle_exception('errorrruleday', 'calendar'); } } /** * Sets the UNTIL rule. * * @param string $until The date string representation of the UNTIL rule. * @throws moodle_exception */ protected function set_until($until) { $this->until = strtotime($until); } /** * Sets the COUNT rule. * * @param string $count The count value. * @throws moodle_exception */ protected function set_count($count) { $this->count = intval($count); } /** * Sets the INTERVAL rule. * * The INTERVAL rule part contains a positive integer representing how often the recurrence rule repeats. * The default value is "1", meaning: * - every second for a SECONDLY rule, or * - every minute for a MINUTELY rule, * - every hour for an HOURLY rule, * - every day for a DAILY rule, * - every week for a WEEKLY rule, * - every month for a MONTHLY rule and * - every year for a YEARLY rule. * * @param string $intervalstr The value for the interval rule. * @throws moodle_exception */ protected function set_interval($intervalstr) { $interval = intval($intervalstr); if ($interval < 1) { throw new moodle_exception('errorinvalidinterval', 'calendar'); } $this->interval = $interval; } /** * Sets the BYSECOND rule. * * The BYSECOND rule part specifies a comma-separated list of seconds within a minute. * Valid values are 0 to 59. * * @param string $bysecond Comma-separated list of seconds within a minute. * @throws moodle_exception */ protected function set_bysecond($bysecond) { $seconds = explode(',', $bysecond); $bysecondrules = []; foreach ($seconds as $second) { if ($second < 0 || $second > 59) { throw new moodle_exception('errorinvalidbysecond', 'calendar'); } $bysecondrules[] = (int)$second; } $this->bysecond = $bysecondrules; } /** * Sets the BYMINUTE rule. * * The BYMINUTE rule part specifies a comma-separated list of seconds within an hour. * Valid values are 0 to 59. * * @param string $byminute Comma-separated list of minutes within an hour. * @throws moodle_exception */ protected function set_byminute($byminute) { $minutes = explode(',', $byminute); $byminuterules = []; foreach ($minutes as $minute) { if ($minute < 0 || $minute > 59) { throw new moodle_exception('errorinvalidbyminute', 'calendar'); } $byminuterules[] = (int)$minute; } $this->byminute = $byminuterules; } /** * Sets the BYHOUR rule. * * The BYHOUR rule part specifies a comma-separated list of hours of the day. * Valid values are 0 to 23. * * @param string $byhour Comma-separated list of hours of the day. * @throws moodle_exception */ protected function set_byhour($byhour) { $hours = explode(',', $byhour); $byhourrules = []; foreach ($hours as $hour) { if ($hour < 0 || $hour > 23) { throw new moodle_exception('errorinvalidbyhour', 'calendar'); } $byhourrules[] = (int)$hour; } $this->byhour = $byhourrules; } /** * Sets the BYDAY rule. * * The BYDAY rule part specifies a comma-separated list of days of the week; * - MO indicates Monday; * - TU indicates Tuesday; * - WE indicates Wednesday; * - TH indicates Thursday; * - FR indicates Friday; * - SA indicates Saturday; * - SU indicates Sunday. * * Each BYDAY value can also be preceded by a positive (+n) or negative (-n) integer. * If present, this indicates the nth occurrence of the specific day within the MONTHLY or YEARLY RRULE. * For example, within a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday within the month, * whereas -1MO represents the last Monday of the month. * If an integer modifier is not present, it means all days of this type within the specified frequency. * For example, within a MONTHLY rule, MO represents all Mondays within the month. * * @param string $byday Comma-separated list of days of the week. * @throws moodle_exception */ protected function set_byday($byday) { $weekdays = array_keys(self::DAYS_OF_WEEK); $days = explode(',', $byday); $bydayrules = []; foreach ($days as $day) { $suffix = substr($day, -2); if (!in_array($suffix, $weekdays)) { throw new moodle_exception('errorinvalidbydaysuffix', 'calendar'); } $bydayrule = new stdClass(); $bydayrule->day = substr($suffix, -2); $bydayrule->value = (int)str_replace($suffix, '', $day); $bydayrules[] = $bydayrule; } $this->byday = $bydayrules; } /** * Sets the BYMONTHDAY rule. * * The BYMONTHDAY rule part specifies a comma-separated list of days of the month. * Valid values are 1 to 31 or -31 to -1. For example, -10 represents the tenth to the last day of the month. * * @param string $bymonthday Comma-separated list of days of the month. * @throws moodle_exception */ protected function set_bymonthday($bymonthday) { $monthdays = explode(',', $bymonthday); $bymonthdayrules = []; foreach ($monthdays as $day) { // Valid values are 1 to 31 or -31 to -1. if ($day < -31 || $day > 31 || $day == 0) { throw new moodle_exception('errorinvalidbymonthday', 'calendar'); } $bymonthdayrules[] = (int)$day; } // Sort these MONTHDAY rules in ascending order. sort($bymonthdayrules); $this->bymonthday = $bymonthdayrules; } /** * Sets the BYYEARDAY rule. * * The BYYEARDAY rule part specifies a comma-separated list of days of the year. * Valid values are 1 to 366 or -366 to -1. For example, -1 represents the last day of the year (December 31st) * and -306 represents the 306th to the last day of the year (March 1st). * * @param string $byyearday Comma-separated list of days of the year. * @throws moodle_exception */ protected function set_byyearday($byyearday) { $yeardays = explode(',', $byyearday); $byyeardayrules = []; foreach ($yeardays as $day) { // Valid values are 1 to 366 or -366 to -1. if ($day < -366 || $day > 366 || $day == 0) { throw new moodle_exception('errorinvalidbyyearday', 'calendar'); } $byyeardayrules[] = (int)$day; } $this->byyearday = $byyeardayrules; } /** * Sets the BYWEEKNO rule. * * The BYWEEKNO rule part specifies a comma-separated list of ordinals specifying weeks of the year. * Valid values are 1 to 53 or -53 to -1. This corresponds to weeks according to week numbering as defined in [ISO 8601]. * A week is defined as a seven day period, starting on the day of the week defined to be the week start (see WKST). * Week number one of the calendar year is the first week which contains at least four (4) days in that calendar year. * This rule part is only valid for YEARLY rules. For example, 3 represents the third week of the year. * * Note: Assuming a Monday week start, week 53 can only occur when Thursday is January 1 or if it is a leap year and Wednesday * is January 1. * * @param string $byweekno Comma-separated list of number of weeks. * @throws moodle_exception */ protected function set_byweekno($byweekno) { $weeknumbers = explode(',', $byweekno); $byweeknorules = []; foreach ($weeknumbers as $week) { // Valid values are 1 to 53 or -53 to -1. if ($week < -53 || $week > 53 || $week == 0) { throw new moodle_exception('errorinvalidbyweekno', 'calendar'); } $byweeknorules[] = (int)$week; } $this->byweekno = $byweeknorules; } /** * Sets the BYMONTH rule. * * The BYMONTH rule part specifies a comma-separated list of months of the year. * Valid values are 1 to 12. * * @param string $bymonth Comma-separated list of months of the year. * @throws moodle_exception */ protected function set_bymonth($bymonth) { $months = explode(',', $bymonth); $bymonthrules = []; foreach ($months as $month) { // Valid values are 1 to 12. if ($month < 1 || $month > 12) { throw new moodle_exception('errorinvalidbymonth', 'calendar'); } $bymonthrules[] = (int)$month; } $this->bymonth = $bymonthrules; } /** * Sets the BYSETPOS rule. * * The BYSETPOS rule part specifies a comma-separated list of values which corresponds to the nth occurrence within the set of * events specified by the rule. Valid values are 1 to 366 or -366 to -1. * It MUST only be used in conjunction with another BYxxx rule part. * * For example "the last work day of the month" could be represented as: RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1 * * @param string $bysetpos Comma-separated list of values. * @throws moodle_exception */ protected function set_bysetpos($bysetpos) { $setposes = explode(',', $bysetpos); $bysetposrules = []; foreach ($setposes as $pos) { // Valid values are 1 to 366 or -366 to -1. if ($pos < -366 || $pos > 366 || $pos == 0) { throw new moodle_exception('errorinvalidbysetpos', 'calendar'); } $bysetposrules[] = (int)$pos; } $this->bysetpos = $bysetposrules; } /** * Validate the rules as a whole. * * @throws moodle_exception */ protected function validate_rules() { // UNTIL and COUNT cannot be in the same recurrence rule. if (!empty($this->until) && !empty($this->count)) { throw new moodle_exception('errorhasuntilandcount', 'calendar'); } // BYSETPOS only be used in conjunction with another BYxxx rule part. if (!empty($this->bysetpos) && empty($this->bymonth) && empty($this->bymonthday) && empty($this->bysecond) && empty($this->byday) && empty($this->byweekno) && empty($this->byhour) && empty($this->byminute) && empty($this->byyearday)) { throw new moodle_exception('errormustbeusedwithotherbyrule', 'calendar'); } // Integer values preceding BYDAY rules can only be present for MONTHLY or YEARLY RRULE. foreach ($this->byday as $bydayrule) { if (!empty($bydayrule->value) && $this->freq != self::FREQ_MONTHLY && $this->freq != self::FREQ_YEARLY) { throw new moodle_exception('errorinvalidbydayprefix', 'calendar'); } } // The BYWEEKNO rule is only valid for YEARLY rules. if (!empty($this->byweekno) && $this->freq != self::FREQ_YEARLY) { throw new moodle_exception('errornonyearlyfreqwithbyweekno', 'calendar'); } } /** * Creates calendar events for the recurring events. * * @param stdClass $event The parent event. * @param int[] $eventtimes The timestamps of the recurring events. */ protected function create_recurring_events($event, $eventtimes) { $count = false; if ($this->count) { $count = $this->count; } foreach ($eventtimes as $time) { // Skip if time is the same time with the parent event's timestamp. if ($time == $event->timestart) { continue; } // Decrement count, if set. if ($count !== false) { $count--; if ($count == 0) { break; } } // Create the recurring event. $cloneevent = clone($event); $cloneevent->repeatid = $event->id; $cloneevent->timestart = $time; unset($cloneevent->id); // UUID should only be set on the first instance of the recurring events. unset($cloneevent->uuid); calendar_event::create($cloneevent, false); } // If COUNT rule is defined and the number of the generated event times is less than the the COUNT rule, // repeat the processing until the COUNT rule is satisfied. if ($count !== false && $count > 0) { // Set count to the remaining counts. $this->count = $count; // Clone the original event, but set the timestart to the last generated event time. $tmpevent = clone($event); $tmpevent->timestart = end($eventtimes); // Generate the additional event times. $additionaleventtimes = $this->generate_recurring_event_times($tmpevent); // Create the additional events. $this->create_recurring_events($event, $additionaleventtimes); } } /** * Generates recurring events based on the parent event and the RRULE set. * * If multiple BYxxx rule parts are specified, then after evaluating the specified FREQ and INTERVAL rule parts, * the BYxxx rule parts are applied to the current set of evaluated occurrences in the following order: * BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, BYHOUR, BYMINUTE, BYSECOND and BYSETPOS; * then COUNT and UNTIL are evaluated. * * @param stdClass $event The event object. * @return array The list of timestamps that obey the given RRULE. */ protected function generate_recurring_event_times($event) { $interval = $this->get_interval(); // Candidate event times. $eventtimes = []; $eventdatetime = new DateTime(date('Y-m-d H:i:s', $event->timestart)); $until = null; if (empty($this->count)) { if ($this->until) { $until = $this->until; } else { // Forever event. However, since there's no such thing as 'forever' (at least not in Moodle), // we only repeat the events until 10 years from the current time. $untildate = new DateTime(); $foreverinterval = new DateInterval('P' . self::TIME_UNLIMITED_YEARS . 'Y'); $untildate->add($foreverinterval); $until = $untildate->getTimestamp(); } } else { // If count is defined, let's define a tentative until date. We'll just trim the number of events later. $untildate = clone($eventdatetime); $count = $this->count; while ($count >= 0) { $untildate->add($interval); $count--; } $until = $untildate->getTimestamp(); } // No filters applied. Generate recurring events right away. if (!$this->has_by_rules()) { // Get initial list of prospective events. $tmpstart = clone($eventdatetime); while ($tmpstart->getTimestamp() <= $until) { $eventtimes[] = $tmpstart->getTimestamp(); $tmpstart->add($interval); } return $eventtimes; } // Get all of potential dates covered by the periods from the event's start date until the last. $dailyinterval = new DateInterval('P1D'); $boundslist = $this->get_period_bounds_list($eventdatetime->getTimestamp(), $until); foreach ($boundslist as $bounds) { $tmpdate = new DateTime(date('Y-m-d H:i:s', $bounds->start)); while ($tmpdate->getTimestamp() >= $bounds->start && $tmpdate->getTimestamp() < $bounds->next) { $eventtimes[] = $tmpdate->getTimestamp(); $tmpdate->add($dailyinterval); } } // Evaluate BYMONTH rules. $eventtimes = $this->filter_by_month($eventtimes); // Evaluate BYWEEKNO rules. $eventtimes = $this->filter_by_weekno($eventtimes); // Evaluate BYYEARDAY rules. $eventtimes = $this->filter_by_yearday($eventtimes); // If BYYEARDAY, BYMONTHDAY and BYDAY are not set, default to BYMONTHDAY based on the DTSTART's day. if ($this->freq != self::FREQ_DAILY && empty($this->byyearday) && empty($this->bymonthday) && empty($this->byday)) { $this->bymonthday = [$eventdatetime->format('j')]; } // Evaluate BYMONTHDAY rules. $eventtimes = $this->filter_by_monthday($eventtimes); // Evaluate BYDAY rules. $eventtimes = $this->filter_by_day($event, $eventtimes, $until); // Evaluate BYHOUR rules. $eventtimes = $this->apply_hour_minute_second_rules($eventdatetime, $eventtimes); // Evaluate BYSETPOS rules. $eventtimes = $this->filter_by_setpos($event, $eventtimes, $until); // Sort event times in ascending order. sort($eventtimes); // Finally, filter candidate event times to make sure they are within the DTSTART and UNTIL/tentative until boundaries. $results = []; foreach ($eventtimes as $time) { // Skip out-of-range events. if ($time < $eventdatetime->getTimestamp()) { continue; } // End if event time is beyond the until limit. if ($time > $until) { break; } $results[] = $time; } return $results; } /** * Generates a DateInterval object based on the FREQ and INTERVAL rules. * * @return DateInterval * @throws moodle_exception */ protected function get_interval() { $intervalspec = null; switch ($this->freq) { case self::FREQ_YEARLY: $intervalspec = 'P' . $this->interval . 'Y'; break; case self::FREQ_MONTHLY: $intervalspec = 'P' . $this->interval . 'M'; break; case self::FREQ_WEEKLY: $intervalspec = 'P' . $this->interval . 'W'; break; case self::FREQ_DAILY: $intervalspec = 'P' . $this->interval . 'D'; break; case self::FREQ_HOURLY: $intervalspec = 'PT' . $this->interval . 'H'; break; case self::FREQ_MINUTELY: $intervalspec = 'PT' . $this->interval . 'M'; break; case self::FREQ_SECONDLY: $intervalspec = 'PT' . $this->interval . 'S'; break; default: // We should never get here, something is very wrong. throw new moodle_exception('errorrrulefreq', 'calendar'); } return new DateInterval($intervalspec); } /** * Determines whether the RRULE has BYxxx rules or not. * * @return bool True if there is one or more BYxxx rules to process. False, otherwise. */ protected function has_by_rules() { return !empty($this->bymonth) || !empty($this->bymonthday) || !empty($this->bysecond) || !empty($this->byday) || !empty($this->byweekno) || !empty($this->byhour) || !empty($this->byminute) || !empty($this->byyearday); } /** * Filter event times based on the BYMONTH rule. * * @param int[] $eventdates Timestamps of event times to be filtered. * @return int[] Array of filtered timestamps. */ protected function filter_by_month($eventdates) { if (empty($this->bymonth)) { return $eventdates; } $filteredbymonth = []; foreach ($eventdates as $time) { foreach ($this->bymonth as $month) { $prospectmonth = date('n', $time); if ($month == $prospectmonth) { $filteredbymonth[] = $time; break; } } } return $filteredbymonth; } /** * Filter event times based on the BYWEEKNO rule. * * @param int[] $eventdates Timestamps of event times to be filtered. * @return int[] Array of filtered timestamps. */ protected function filter_by_weekno($eventdates) { if (empty($this->byweekno)) { return $eventdates; } $filteredbyweekno = []; $weeklyinterval = null; foreach ($eventdates as $time) { $tmpdate = new DateTime(date('Y-m-d H:i:s', $time)); foreach ($this->byweekno as $weekno) { if ($weekno > 0) { if ($tmpdate->format('W') == $weekno) { $filteredbyweekno[] = $time; break; } } else if ($weekno < 0) { if ($weeklyinterval === null) { $weeklyinterval = new DateInterval('P1W'); } $weekstart = new DateTime(); $weekstart->setISODate($tmpdate->format('Y'), $weekno); $weeknext = clone($weekstart); $weeknext->add($weeklyinterval); $tmptimestamp = $tmpdate->getTimestamp(); if ($tmptimestamp >= $weekstart->getTimestamp() && $tmptimestamp < $weeknext->getTimestamp()) { $filteredbyweekno[] = $time; break; } } } } return $filteredbyweekno; } /** * Filter event times based on the BYYEARDAY rule. * * @param int[] $eventdates Timestamps of event times to be filtered. * @return int[] Array of filtered timestamps. */ protected function filter_by_yearday($eventdates) { if (empty($this->byyearday)) { return $eventdates; } $filteredbyyearday = []; foreach ($eventdates as $time) { $tmpdate = new DateTime(date('Y-m-d', $time)); foreach ($this->byyearday as $yearday) { $dayoffset = abs($yearday) - 1; $dayoffsetinterval = new DateInterval("P{$dayoffset}D"); if ($yearday > 0) { $tmpyearday = (int)$tmpdate->format('z') + 1; if ($tmpyearday == $yearday) { $filteredbyyearday[] = $time; break; } } else if ($yearday < 0) { $yeardaydate = new DateTime('last day of ' . $tmpdate->format('Y')); $yeardaydate->sub($dayoffsetinterval); $tmpdate->getTimestamp(); if ($yeardaydate->format('z') == $tmpdate->format('z')) { $filteredbyyearday[] = $time; break; } } } } return $filteredbyyearday; } /** * Filter event times based on the BYMONTHDAY rule. * * @param int[] $eventdates The event times to be filtered. * @return int[] Array of filtered timestamps. */ protected function filter_by_monthday($eventdates) { if (empty($this->bymonthday)) { return $eventdates; } $filteredbymonthday = []; foreach ($eventdates as $time) { $eventdatetime = new DateTime(date('Y-m-d', $time)); foreach ($this->bymonthday as $monthday) { // Days to add/subtract. $daysoffset = abs($monthday) - 1; $dayinterval = new DateInterval("P{$daysoffset}D"); if ($monthday > 0) { if ($eventdatetime->format('j') == $monthday) { $filteredbymonthday[] = $time; break; } } else if ($monthday < 0) { $tmpdate = clone($eventdatetime); // Reset to the first day of the month. $tmpdate->modify('first day of this month'); // Then go to last day of the month. $tmpdate->modify('last day of this month'); if ($daysoffset > 0) { // Then subtract the monthday value. $tmpdate->sub($dayinterval); } if ($eventdatetime->format('j') == $tmpdate->format('j')) { $filteredbymonthday[] = $time; break; } } } } return $filteredbymonthday; } /** * Filter event times based on the BYDAY rule. * * @param stdClass $event The parent event. * @param int[] $eventdates The event times to be filtered. * @param int $until Event times generation limit date. * @return int[] Array of filtered timestamps. */ protected function filter_by_day($event, $eventdates, $until) { if (empty($this->byday)) { return $eventdates; } $filteredbyday = []; $bounds = $this->get_period_bounds_list($event->timestart, $until); $nextmonthinterval = new DateInterval('P1M'); foreach ($eventdates as $time) { $tmpdatetime = new DateTime(date('Y-m-d', $time)); foreach ($this->byday as $day) { $dayname = self::DAYS_OF_WEEK[$day->day]; // Skip if they day name of the event time does not match the day part of the BYDAY rule. if ($tmpdatetime->format('l') !== $dayname) { continue; } if (empty($day->value)) { // No modifier value. Applies to all weekdays of the given period. $filteredbyday[] = $time; break; } else if ($day->value > 0) { // Positive value. if ($this->freq === self::FREQ_YEARLY && empty($this->bymonth)) { // Get the first day of the year. $firstdaydate = $tmpdatetime->format('Y') . '-01-01'; } else { // Get the first day of the month. $firstdaydate = $tmpdatetime->format('Y-m') . '-01'; } $expecteddate = new DateTime($firstdaydate); $count = $day->value; // Get the nth week day of the year/month. $expecteddate->modify("+$count $dayname"); if ($expecteddate->format('Y-m-d') === $tmpdatetime->format('Y-m-d')) { $filteredbyday[] = $time; break; } } else { // Negative value. $count = $day->value; if ($this->freq === self::FREQ_YEARLY && empty($this->bymonth)) { // The -Nth week day of the year. $eventyear = (int)$tmpdatetime->format('Y'); // Get temporary DateTime object starting from the first day of the next year. $expecteddate = new DateTime((++$eventyear) . '-01-01'); while ($count < 0) { // Get the start of the previous week. $expecteddate->modify('last ' . $this->wkst); $tmpexpecteddate = clone($expecteddate); if ($tmpexpecteddate->format('l') !== $dayname) { $tmpexpecteddate->modify('next ' . $dayname); } if ($this->in_bounds($tmpexpecteddate->getTimestamp(), $bounds)) { $expecteddate = $tmpexpecteddate; $count++; } } if ($expecteddate->format('l') !== $dayname) { $expecteddate->modify('next ' . $dayname); } if ($expecteddate->getTimestamp() == $time) { $filteredbyday[] = $time; break; } } else { // The -Nth week day of the month. $expectedmonthyear = $tmpdatetime->format('F Y'); $expecteddate = new DateTime("first day of $expectedmonthyear"); $expecteddate->add($nextmonthinterval); while ($count < 0) { // Get the start of the previous week. $expecteddate->modify('last ' . $this->wkst); $tmpexpecteddate = clone($expecteddate); if ($tmpexpecteddate->format('l') !== $dayname) { $tmpexpecteddate->modify('next ' . $dayname); } if ($this->in_bounds($tmpexpecteddate->getTimestamp(), $bounds)) { $expecteddate = $tmpexpecteddate; $count++; } } // Compare the expected date with the event's timestamp. if ($expecteddate->getTimestamp() == $time) { $filteredbyday[] = $time; break; } } } } } return $filteredbyday; } /** * Applies BYHOUR, BYMINUTE and BYSECOND rules to the calculated event dates. * Defaults to the DTSTART's hour/minute/second component when not defined. * * @param DateTime $eventdatetime The parent event DateTime object pertaining to the DTSTART. * @param int[] $eventdates Array of candidate event date timestamps. * @return array List of updated event timestamps that contain the time component of the event times. */ protected function apply_hour_minute_second_rules(DateTime $eventdatetime, $eventdates) { // If BYHOUR rules are not set, set the hour of the events from the DTSTART's hour component. if (empty($this->byhour)) { $this->byhour = [$eventdatetime->format('G')]; } // If BYMINUTE rules are not set, set the hour of the events from the DTSTART's minute component. if (empty($this->byminute)) { $this->byminute = [(int)$eventdatetime->format('i')]; } // If BYSECOND rules are not set, set the hour of the events from the DTSTART's second component. if (empty($this->bysecond)) { $this->bysecond = [(int)$eventdatetime->format('s')]; } $results = []; foreach ($eventdates as $time) { $datetime = new DateTime(date('Y-m-d', $time)); foreach ($this->byhour as $hour) { foreach ($this->byminute as $minute) { foreach ($this->bysecond as $second) { $datetime->setTime($hour, $minute, $second); $results[] = $datetime->getTimestamp(); } } } } return $results; } /** * Filter event times based on the BYSETPOS rule. * * @param stdClass $event The parent event. * @param int[] $eventtimes The event times to be filtered. * @param int $until Event times generation limit date. * @return int[] Array of filtered timestamps. */ protected function filter_by_setpos($event, $eventtimes, $until) { if (empty($this->bysetpos)) { return $eventtimes; } $filteredbysetpos = []; $boundslist = $this->get_period_bounds_list($event->timestart, $until); sort($eventtimes); foreach ($boundslist as $bounds) { // Generate a list of candidate event times based that are covered in a period's bounds. $prospecttimes = []; foreach ($eventtimes as $time) { if ($time >= $bounds->start && $time < $bounds->next) { $prospecttimes[] = $time; } } if (empty($prospecttimes)) { continue; } // Add the event times that correspond to the set position rule into the filtered results. foreach ($this->bysetpos as $pos) { $tmptimes = $prospecttimes; if ($pos < 0) { rsort($tmptimes); } $index = abs($pos) - 1; if (isset($tmptimes[$index])) { $filteredbysetpos[] = $tmptimes[$index]; } } } return $filteredbysetpos; } /** * Gets the list of period boundaries covered by the recurring events. * * @param int $eventtime The event timestamp. * @param int $until The end timestamp. * @return array List of period bounds, with start and next properties. */ protected function get_period_bounds_list($eventtime, $until) { $interval = $this->get_interval(); $periodbounds = $this->get_period_boundaries($eventtime); $periodstart = $periodbounds['start']; $periodafter = $periodbounds['next']; $bounds = []; if ($until !== null) { while ($periodstart->getTimestamp() < $until) { $bounds[] = (object)[ 'start' => $periodstart->getTimestamp(), 'next' => $periodafter->getTimestamp() ]; $periodstart->add($interval); $periodafter->add($interval); } } else { $count = $this->count; while ($count > 0) { $bounds[] = (object)[ 'start' => $periodstart->getTimestamp(), 'next' => $periodafter->getTimestamp() ]; $periodstart->add($interval); $periodafter->add($interval); $count--; } } return $bounds; } /** * Determine whether the date-time in question is within the bounds of the periods that are covered by the RRULE. * * @param int $time The timestamp to be evaluated. * @param array $bounds Array of period boundaries covered by the RRULE. * @return bool */ protected function in_bounds($time, $bounds) { foreach ($bounds as $bound) { if ($time >= $bound->start && $time < $bound->next) { return true; } } return false; } /** * Determines the start and end DateTime objects that serve as references to determine whether a calculated event timestamp * falls on the period defined by these DateTimes objects. * * @param int $eventtime Unix timestamp of the event time. * @return DateTime[] * @throws moodle_exception */ protected function get_period_boundaries($eventtime) { $nextintervalspec = null; switch ($this->freq) { case self::FREQ_YEARLY: $nextintervalspec = 'P1Y'; $timestart = date('Y-01-01', $eventtime); break; case self::FREQ_MONTHLY: $nextintervalspec = 'P1M'; $timestart = date('Y-m-01', $eventtime); break; case self::FREQ_WEEKLY: $nextintervalspec = 'P1W'; if (date('l', $eventtime) === $this->wkst) { $weekstarttime = $eventtime; } else { $weekstarttime = strtotime('last ' . $this->wkst, $eventtime); } $timestart = date('Y-m-d', $weekstarttime); break; case self::FREQ_DAILY: $nextintervalspec = 'P1D'; $timestart = date('Y-m-d', $eventtime); break; case self::FREQ_HOURLY: $nextintervalspec = 'PT1H'; $timestart = date('Y-m-d H:00:00', $eventtime); break; case self::FREQ_MINUTELY: $nextintervalspec = 'PT1M'; $timestart = date('Y-m-d H:i:00', $eventtime); break; case self::FREQ_SECONDLY: $nextintervalspec = 'PT1S'; $timestart = date('Y-m-d H:i:s', $eventtime); break; default: // We should never get here, something is very wrong. throw new moodle_exception('errorrrulefreq', 'calendar'); } $eventstart = new DateTime($timestart); $eventnext = clone($eventstart); $nextinterval = new DateInterval($nextintervalspec); $eventnext->add($nextinterval); return [ 'start' => $eventstart, 'next' => $eventnext, ]; } } PK*\hclasses/action_factory.phpnu[. /** * Action factory. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar; defined('MOODLE_INTERNAL') || die(); use core_calendar\local\event\factories\action_factory_interface; use core_calendar\local\event\value_objects\action; /** * Action factory class. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class action_factory implements action_factory_interface { public function create_instance($name, \moodle_url $url, $itemcount, $actionable) { return new action ($name, $url, $itemcount, $actionable); } } PK*\$$classes/export_form.phpnu[. /** * The mform for exporting calendar events * * @package core_calendar * @copyright 2014 Brian Barnes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // Always include formslib. if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); // It must be included from a Moodle page. } require_once($CFG->dirroot.'/lib/formslib.php'); /** * The mform class for creating and editing a calendar * * @copyright 2014 Brian Barnes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_calendar_export_form extends moodleform { /** * The export form definition * @throws coding_exception */ public function definition() { global $CFG; $mform = $this->_form; $mform->addElement('html', '
' . get_string('exporthelp', 'calendar') . '
'); $export = array(); $export[] = $mform->createElement('radio', 'exportevents', '', get_string('eventsall', 'calendar'), 'all'); $export[] = $mform->createElement('radio', 'exportevents', '', get_string('eventsrelatedtocategories', 'calendar'), 'categories'); $export[] = $mform->createElement('radio', 'exportevents', '', get_string('eventsrelatedtocourses', 'calendar'), 'courses'); $export[] = $mform->createElement('radio', 'exportevents', '', get_string('eventsrelatedtogroups', 'calendar'), 'groups'); $export[] = $mform->createElement('radio', 'exportevents', '', get_string('eventspersonal', 'calendar'), 'user'); $mform->addGroup($export, 'events', get_string('eventstoexport', 'calendar'), '
'); $mform->addGroupRule('events', get_string('required'), 'required'); $mform->setDefault('events', 'all'); $range = array(); if ($this->_customdata['allowthisweek']) { $range[] = $mform->createElement('radio', 'timeperiod', '', get_string('weekthis', 'calendar'), 'weeknow'); } if ($this->_customdata['allownextweek']) { $range[] = $mform->createElement('radio', 'timeperiod', '', get_string('weeknext', 'calendar'), 'weeknext'); } $range[] = $mform->createElement('radio', 'timeperiod', '', get_string('monththis', 'calendar'), 'monthnow'); if ($this->_customdata['allownextmonth']) { $range[] = $mform->createElement('radio', 'timeperiod', '', get_string('monthnext', 'calendar'), 'monthnext'); } $range[] = $mform->createElement('radio', 'timeperiod', '', get_string('recentupcoming', 'calendar'), 'recentupcoming'); if ($CFG->calendar_customexport) { $a = new stdClass(); $now = time(); $time = $now - $CFG->calendar_exportlookback * DAYSECS; $a->timestart = userdate($time, get_string('strftimedatefullshort', 'langconfig')); $time = $now + $CFG->calendar_exportlookahead * DAYSECS; $a->timeend = userdate($time, get_string('strftimedatefullshort', 'langconfig')); $range[] = $mform->createElement('radio', 'timeperiod', '', get_string('customexport', 'calendar', $a), 'custom'); } $mform->addGroup($range, 'period', get_string('timeperiod', 'calendar'), '
'); $mform->addGroupRule('period', get_string('required'), 'required'); $mform->setDefault('period', 'recentupcoming'); $buttons = array(); $buttons[] = $mform->createElement('submit', 'generateurl', get_string('generateurlbutton', 'calendar')); $buttons[] = $mform->createElement('submit', 'export', get_string('exportbutton', 'calendar')); $mform->addGroup($buttons); } } PK*\5!%%!templates/month_detailed.mustachenu[{{! 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 . }} {{! @template core_calendar/month_detailed Calendar month view. The purpose of this template is to render the month view. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{> core_calendar/month_navigation }} {{> core/overlay_loading}} {{# daynames }} {{/ daynames }} {{#weeks}} {{#prepadding}} {{/prepadding}} {{#days}} {{/days}} {{#postpadding}} {{/postpadding}} {{/weeks}}
{{fullname}}
 
{{daytitle}} {{#hasevents}} {{mday}} {{/hasevents}} {{^hasevents}} {{/hasevents}} {{#hasevents}}
{{/hasevents}}
{{daytitle}} {{#hasevents}} {{mday}} {{/hasevents}} {{^hasevents}} {{/hasevents}}
 
{{#js}} require([ 'jquery', 'core_calendar/month_view_drag_drop' ], function( $, DragDrop ) { var root = $('#month-detailed-{{uniqid}}-{{calendarinstanceid}}'); DragDrop.init(root); }); {{/js}} PK*\D:}}&templates/event_summary_modal.mustachenu[{{! 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 . }} {{! @template core_calendar/event_summary_modal This template renders the calendar events summary modal. Example context (json): { "title": "Assignment due 1" } }} {{< core/modal }} {{$footer}} {{#candelete}} {{/candelete}} {{#action.actionable}} {{{action.name}}} {{/action.actionable}} {{^action.actionable}} {{#isactionevent}} {{#str}} gotoactivity, core_calendar {{/str}} {{/isactionevent}} {{/action.actionable}} {{^isactionevent}} {{#canedit}} {{/canedit}} {{/isactionevent}} {{/footer}} {{/ core/modal }} PK*\L templates/calendar_mini.mustachenu[{{! 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 . }} {{! @template core_calendar/calendar_mini Calendar month view for blocks. The purpose of this template is to render the month view for blocks. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{> core_calendar/month_mini}} {{#js}} require(['jquery', 'core_calendar/calendar_mini'], function($, CalendarMini) { CalendarMini.init($("#calendar-month-{{date.year}}-{{date.mon}}-{{uniqid}}"), !{{initialeventsloaded}}); }); {{/js}} PK*\A--templates/event_filter.mustachenu[{{! 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 . }} {{! @template core_calendar/event_filter Filter to select which event types displayed on this page. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }}
    {{#eventtypes}}
  • {{> core_calendar/event_filter_key }}
  • {{/eventtypes}}
{{#js}} require(['jquery', 'core_calendar/calendar_filter'], function($, CalendarFilter) { CalendarFilter.init($("#event-filter-{{uniqid}}")); }); {{/js}} PK*\ templates/upcoming_mini.mustachenu[{{! 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 . }} {{! @template core_calendar/upcoming_mini Calendar upcoming view for blocks. The purpose of this template is to render the upcoming view for blocks. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }}
{{> core/overlay_loading}} {{#events}}
{{#icon}} {{#iconurl}} {{alttext}} {{/iconurl}} {{^iconurl}} {{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}} {{/iconurl}} {{/icon}}

{{{name}}}

{{{formattedtime}}}
{{/events}} {{^events}} {{#str}}noupcomingevents, calendar{{/str}} {{/events}} {{#js}} require([ 'jquery', 'core_calendar/selectors', 'core_calendar/events', ], function( $, CalendarSelectors, CalendarEvents ) { var root = $('#month-upcoming-mini-{{uniqid}}'); $('body').on(CalendarEvents.filterChanged, function(e, data) { M.util.js_pending("month-upcoming-mini-{{uniqid}}-filterChanged"); // A filter value has been changed. // Find all matching cells in the popover data, and hide them. var target = $("#month-upcoming-mini-{{uniqid}}").find(CalendarSelectors.eventType[data.type]); var transitionPromise = $.Deferred(); if (data.hidden) { transitionPromise.then(function() { return target.slideUp('fast').promise(); }); } else { transitionPromise.then(function() { return target.slideDown('fast').promise(); }); } transitionPromise.then(function() { M.util.js_complete("month-upcoming-mini-{{uniqid}}-filterChanged"); return; }); transitionPromise.resolve(); }); }); {{/js}} PK*\!templates/footer_options.mustachenu[{{! 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 . }} {{! @template core_calendar/footer_options Displays export options on the calendar footer. Example context (json): { "footerlinks": [ { "url": "calendar/view.php?view=month", "linkname": "Full calendar" }, { "url": "calendar/managesubscriptions.php", "linkname": "Manage subcriptions" } ] } }}
{{#footerlinks}} {{linkname}} {{/footerlinks}}
PK*\WúCC#templates/add_event_button.mustachenu[{{! 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 . }} {{! @template core_calendar/add_event_button Button to launch the "Add new event" dialogue. The purpose of this template is to render the button used to generate the new event dialogue. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { "defaulteventcontext": "2" } }} {{#defaulteventcontext}} {{#str}}newevent, core_calendar{{/str}} {{/defaulteventcontext}} PK*\!templates/day_navigation.mustachenu[{{! 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 . }} {{! @template core_calendar/day_navigation Calendar day navigation. The purpose of this template is to render the navigation to switch to previous and next months. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }}
  {{previousperiodname}} |

{{periodname}}

| {{nextperiodname}}  
PK*\))$templates/calendar_upcoming.mustachenu[{{! 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 . }} {{! @template core_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): { } }}
{{> core_calendar/header}} {{> core_calendar/upcoming_detailed}}
{{#js}} require(['jquery', 'core_calendar/calendar_view'], function($, CalendarView) { CalendarView.init($("#calendar-upcoming-{{uniqid}}"), 'upcoming'); }); {{/js}} PK*\d%Atemplates/event_icon.mustachenu[{{! 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 . }} {{! @template core_calendar/event_icon Event icon display. The purpose of this template is to provide a common location for the rendering of event icons. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { "modulename": "assign" } }} {{#modulename}} {{#icon}} {{#iconurl}} {{alttext}} {{/iconurl}} {{^iconurl}} {{#pix}} monologo, {{modulename}} {{/pix}} {{/iconurl}} {{/icon}} {{^icon}} {{#pix}} monologo, {{modulename}} {{/pix}} {{/icon}} {{/modulename}} {{^modulename}} {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}} {{/modulename}} PK*\,%templates/event_list.mustachenu[{{! 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 . }} {{! @template core_calendar/event_list Calendar event list. The purpose of this template is to render a list of events. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }}
{{#events}} {{> core_calendar/event_item }} {{/events}} {{^events}} {{$noeventsmessage}}{{/noeventsmessage}} {{/events}}
PK*\ templates/event_details.mustachenu[{{! 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 . }} {{! @template core_calendar/event_details Calendar event details. The purpose of this template is to render the event details. This template is used in the summary modal, day and upcoming views to output event information consistently across the calendar. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { "formattedtime": "Wednesday, 17 April, 9:27 AM", "formattedlocation": "Moodle HQ", "normalisedeventtype": "Group", "normalisedeventtypetext": "Group event", "description": "An random event description", "isactionevent": "true", "course": { "viewurl": "http://mymoodlesite/course/view.php?id=1", "fullname": "Course name" }, "source": "Ical imported", "groupname": "Group 1", "iscategoryevent": "true", "category": { "nestedname": "Category name" }, "iscourseevent": "true", "groupname": "Group name", "subscription": "true", "displayeventsource": "true", "subscriptionname": "Subscription name", "subscriptionurl": "http://subscriptionurl" } }}
{{#pix}} i/calendareventtime, core, {{#str}} when, core_calendar {{/str}} {{/pix}}
{{{formattedtime}}}
{{#normalisedeventtypetext}}
{{#pix}} i/calendar, core, {{#str}} eventtype, core_calendar {{/str}} {{/pix}}
{{normalisedeventtypetext}}
{{/normalisedeventtypetext}} {{#description}}
{{#pix}} i/calendareventdescription, core, {{#str}} description {{/str}} {{/pix}}
{{{.}}}
{{/description}} {{#formattedlocation}}
{{#pix}} i/location, core, {{#str}} location {{/str}} {{/pix}}
{{{.}}}
{{/formattedlocation}} {{#iscategoryevent}}
{{#pix}} i/categoryevent, core, {{#str}} category {{/str}} {{/pix}}
{{{category.nestedname}}}
{{/iscategoryevent}} {{#course.viewurl}}
{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}
{{/course.viewurl}} {{#groupname}}
{{#pix}} i/groupevent, core, {{#str}} group {{/str}} {{/pix}}
{{{groupname}}}
{{/groupname}} {{#subscription}} {{#displayeventsource}}
{{#pix}} i/rss, core, {{#str}} eventsource, core_calendar {{/str}} {{/pix}}
{{#subscriptionurl}}

{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}

{{/subscriptionurl}} {{^subscriptionurl}}

{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}

{{/subscriptionurl}}
{{/displayeventsource}} {{/subscription}} PK*\templates/calendar_day.mustachenu[{{! 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 . }} {{! @template core_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): { "iscalendarblock": false } }}
{{> core_calendar/header}} {{> core_calendar/day_detailed}}
{{#js}} require(['jquery', 'core_calendar/calendar_view'], function($, CalendarView) { const isCalendarBlock = {{#iscalendarblock}}true{{/iscalendarblock}}{{^iscalendarblock}}false{{/iscalendarblock}}; CalendarView.init($("#calendar-day-{{uniqid}}"), 'day', isCalendarBlock); }); {{/js}} PK*\ܤ #templates/month_navigation.mustachenu[{{! 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 . }} {{! @template core_calendar/month_navigation Calendar month navigation. The purpose of this template is to render the navigation to switch to previous and next months. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }}
  {{previousperiodname}} | {{#viewinginblock}}

{{periodname}}

{{/viewinginblock}} {{^viewinginblock}}

{{periodname}}

{{/viewinginblock}} | {{nextperiodname}}  
{{#js}} require(['jquery', 'core_calendar/month_navigation_drag_drop'], function($, DragDrop) { var root = $('#month-navigation-{{uniqid}}-{{calendarinstanceid}}'); DragDrop.init(root); }); {{/js}} PK*\5țtemplates/day_detailed.mustachenu[{{! 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 . }} {{! @template core_calendar/day_detailed 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): { } }} {{> core_calendar/day_navigation }} {{> core/overlay_loading}} {{< core_calendar/event_list }} {{$noeventsmessage}} {{#str}} daywithnoevents, core_calendar {{/str}} {{/noeventsmessage}} {{/core_calendar/event_list}} PK*\8TCC-templates/subscription_update_result.mustachenu[{{! 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 . }} {{! @template core_calendar/subscription_update_result Calendar subscription update result. The purpose of this template is to render the calendar subscription update result. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { "subscriptionname": "Moodle", "eventsimported": 1, "eventsskipped": 2, "eventsupdated": 3, "deletedcount": 4, "errors": [ "Event 1", "Event 2" ] } }} {{#subscriptionname}}

{{#str}} subscriptionupdated, calendar, {{subscriptionname}} {{/str}}

{{/subscriptionname}}
  • {{#str}} eventsimported, calendar, {{eventsimported}} {{/str}}
  • {{#str}} eventsskipped, calendar, {{eventsskipped}} {{/str}}
  • {{#str}} eventsupdated, calendar, {{eventsupdated}} {{/str}}
  • {{#str}} eventsdeleted, calendar, {{eventsdeleted}} {{/str}}
{{#haserror}}

{{#str}} erroraddingevent, calendar {{/str}}

    {{#eventserror}}
  • {{.}}
  • {{/eventserror}}
{{/haserror}} PK*\ptemplates/humandate.mustachenu[{{! 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 . }} {{! @template core_calendar/humandate Print a human readable date format. Example context (json): { "userdate": "2021-09-01", "date": "Tomorrow, 23 January", "time": "12:00 AM", "timestamp": "1630512000", "ispast": false, "isnear": true, "nearicon": { "extraclasses": "me-0 pb-1", "attributes": [ {"name": "src", "value": "../../../pix/i/warning.svg"}, {"name": "alt", "value": "Warning"} ] }, "needtitle": true, "link": "https://example.com/calendar/view.php" } }} {{#link}} {{/link}} {{#nearicon}} {{>core/pix_icon}} {{/nearicon}} {{#date}}{{date}}{{#time}}, {{/time}}{{/date}}{{time}} {{#link}} {{/link}} PK*\+d!templates/month_mini.mustachenu[{{! 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 . }} {{! @template core_calendar/month_mini Calendar month view for blocks. The purpose of this template is to render the month view for blocks. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{> core/overlay_loading}} {{# daynames }} {{/ daynames }} {{#weeks}} {{#prepadding}} {{/prepadding}} {{#days}} {{/days}} {{#postpadding}} {{/postpadding}} {{/weeks}}
{{#includenavigation}} | {{periodname}} | {{/includenavigation}} {{^includenavigation}}

{{periodname}}

{{/includenavigation}}
{{fullname}}
 {{! }}{{daytitle}} {{#popovertitle}} {{< core_calendar/minicalendar_day_link }} {{$day}}{{mday}}{{/day}} {{$url}}{{viewdaylink}}{{/url}} {{$title}}{{popovertitle}}{{/title}} {{$nocontent}}{{#str}}eventnone, calendar{{/str}}{{/nocontent}} {{$content}} {{#events}}
{{> core_calendar/event_icon}} {{{popupname}}}
{{/events}} {{/content}} {{/ core_calendar/minicalendar_day_link }} {{/popovertitle}}{{! }}{{^popovertitle}} {{/popovertitle}}{{! }}
 
{{#js}} require([ 'jquery', 'core_calendar/selectors', 'core_calendar/events', ], function( $, CalendarSelectors, CalendarEvents ) { $('body').on(CalendarEvents.filterChanged, function(e, data) { M.util.js_pending("month-mini-{{uniqid}}-filterChanged"); // A filter value has been changed. // Find all matching cells in the popover data, and hide them. $("#month-mini-{{date.year}}-{{date.mon}}-{{uniqid}}") .find(CalendarSelectors.popoverType[data.type]) .toggleClass('hidden', !!data.hidden); M.util.js_complete("month-mini-{{uniqid}}-filterChanged"); }); }); {{/js}} PK*\"templates/event_item.mustachenu[{{! 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 . }} {{! @template core_calendar/event_item Calendar event item. The purpose of this template is to render the event item. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { "id": 1, "name": "Sample event name", "normalisedeventtype": "course", "course": { "id": 1 }, "canedit": true, "candelete": true, "isactionevent": true, "icon": { "key": "i/courseevent", "component": "core", "alttext": "Some course event", "iconurl": "#" }, "editurl": "#", "url": "#" } }}
{{#canedit}} {{#candelete}} {{#pix}}t/delete, core{{/pix}} {{/candelete}} {{^isactionevent}} {{#pix}}t/edit, core{{/pix}} {{/isactionevent}} {{/canedit}}
{{#icon}}
{{#iconurl}} {{alttext}} {{/iconurl}} {{^iconurl}} {{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}} {{/iconurl}}
{{/icon}}

{{{name}}}

{{> core_calendar/event_details }}
{{#action.actionable}} {{/action.actionable}} {{^action.actionable}} {{#isactionevent}} {{/isactionevent}} {{/action.actionable}}
PK*\}*!templates/calendar_month.mustachenu[{{! 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 . }} {{! @template core_calendar/calendar_month Calendar month view for blcoks. The purpose of this template is to render the main, detailed monthly view. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { "iscalendarblock": false } }}
{{> core_calendar/header}} {{> core_calendar/month_detailed}}
{{#js}} require(['jquery', 'core_calendar/calendar', 'core_calendar/popover'], function($, Calendar, calendarPopover) { const isCalendarBlock = {{#iscalendarblock}}true{{/iscalendarblock}}{{^iscalendarblock}}false{{/iscalendarblock}}; Calendar.init($("#calendar-month-{{uniqid}}-{{calendarinstanceid}}"), isCalendarBlock); }); {{/js}} PK*\>ԝ$templates/upcoming_detailed.mustachenu[{{! 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 . }} {{! @template core_calendar/upcoming_detailed Calendar upcoming detailed. The purpose of this template is to render the calendar upcoming detailed view. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }}
{{> core/overlay_loading}}

{{#str}}upcomingevents, core_calendar{{/str}}

{{< core_calendar/event_list }} {{$noeventsmessage}} {{#str}} noupcomingevents, core_calendar {{/str}} {{/noeventsmessage}} {{/core_calendar/event_list}}
PK*\ e (templates/minicalendar_day_link.mustachenu[{{! 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 . }} {{! @template core_calendar/minicalendar_day_link Displays link to calendar day in mini calendar, with hover tooltip. Example context (json): { "day": "Today", "url": "http://example.com/", "title": "Monday 2nd January", "content": "Test site event" } }} {{$day}}{{day}}{{/day}} {{#js}} require(['jquery'], function($) { require(['theme_boost/bootstrap/popover'], function() { const target = document.getElementById("calendar-day-popover-link-{{courseid}}-{{year}}-{{yday}}-{{uniqid}}"); new Popover(target, { content: function() { var source = target.next().find("> *:not('.hidden')"); var content = $('
'); if (source.length) { content.html(source.clone(false)); } else { content.html(target.data('alternate')); } return content.html(); } }); }); }); {{/js}} PK*\&qq#templates/event_filter_key.mustachenu[{{! 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 . }} {{! @template core_calendar/event_filter_key Key item for the event filter. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{^hidden}} {{#pix}}i/hide, core, {{#str}}hideeventtype, calendar, {{name}}{{/str}}{{/pix}}   {{#str}}hideeventtype, calendar, {{name}}{{/str}} {{/hidden}} {{#hidden}} {{#pix}}i/show, core, {{#str}}showeventtype, calendar, {{name}}{{/str}}{{/pix}}   {{#str}}showeventtype, calendar, {{name}}{{/str}} {{/hidden}} PK*\?L%templates/event_delete_modal.mustachenu[{{! 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 . }} {{! @template core_calendar/event_delete_modal Moodle modal template with save and cancel buttons. The purpose of this template is to render a modal. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * title A cleaned string (use clean_text()) to display. * body HTML content for the boday Example context (json): { "title": "Example delete event seriesmodal", "body": "Do you want to delete this event, or all events in the series?" } }} {{< core/modal }} {{$footer}} {{/footer}} {{/ core/modal }} PK*\%33)templates/calendar_upcoming_mini.mustachenu[{{! 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 . }} {{! @template core_calendar/calendar_upcoming_mini 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): { } }}
{{> core_calendar/upcoming_mini}}
{{#isloggedin}} {{#js}} require(['jquery', 'core_calendar/calendar_view'], function($, CalendarView) { CalendarView.init($("#calendar-upcoming-block-{{uniqid}}"), 'upcoming'); }); {{/js}} {{/isloggedin}} PK*\> templates/header.mustachenu[{{! 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 . }} {{! @template core_calendar/header Calendar header. The purpose of this template is to render the month header. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }}
{{#showviewselector}} {{> core_calendar/view_selector}} {{/showviewselector}} {{#filter_selector}} {{{filter_selector}}} {{/filter_selector}} {{> core_calendar/add_event_button}}
PK*\M"&templates/export_calendar_url.mustachenu[{{! 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 . }} {{! @template core_calendar/export_calendar_url Template for the URL for the exported calendar. Example context (json): { "calendarexporturl": "https://www.moodle.org" } }}
{{#js}} require(['core_calendar/export'], function(calendarExport) { calendarExport.init(); }); {{/js}} PK*\1Njj#templates/modal_event_form.mustachenu[{{! 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 . }} {{! @template core_calendar/modal_event_form Moodle modal template with save and cancel buttons. The purpose of this template is to render a modal. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * title A cleaned string (use clean_text()) to display. * body HTML content for the boday Example context (json): { "title": "Example save cancel modal", "body": "Some example content for the body" } }} {{< core/modal }} {{$footer}} {{/footer}} {{/ core/modal }} PK*\Pm m templates/view_selector.mustachenu[{{! 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 . }} {{! @template core_calendar/view_selector This template renders display dropdown. Example context (json): { "viewingmonth": true, "viewingday": false, "viewingupcoming": false } }}
PK*\.%templates/event_summary_body.mustachenu[{{! 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 . }} {{! @template core_calendar/event_summary_body This template renders the body of calendar events summary modal. Example context (json): { "timestart": 1490320388, "description": "An random event description", "location": "13th floor, building 42", "eventtype": "User", "source": "Ical imported", "groupname": "Group 1" } }}
{{> core_calendar/event_details }}
PK*\&G"templates/humantimeperiod.mustachenu[{{! 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 . }} {{! @template core_calendar/humantimeperiod Print a human readable time period. Example context (json): { "startdate": { "userdate": "2021-09-01", "date": "Tomorrow 12:00 AM", "timestamp": "1630512000", "ispast": false, "isnear": true, "nearicon": { "extraclasses": "me-0 pb-1", "attributes": [ {"name": "src", "value": "../../../pix/i/warning.svg"}, {"name": "alt", "value": "Warning"} ] }, "needtitle": true }, "enddate": { "userdate": "2021-09-01", "date": "Tomorrow 12:00 AM", "timestamp": "1630512000", "ispast": false, "isnear": true, "nearicon": { "extraclasses": "me-0 pb-1", "attributes": [ {"name": "src", "value": "../../../pix/i/warning.svg"}, {"name": "alt", "value": "Warning"} ] }, "needtitle": true } } }} {{#startdate}} {{> core_calendar/humandate }} {{/startdate}} {{#enddate}} » {{> core_calendar/humandate }} {{/enddate}} PK*\Y%templates/event_subscription.mustachenu[{{! 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 . }} {{! @template core_calendar/event_subscription Calendar event subscription. The purpose of this template is to render the event subscription data. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{#subscription}} {{#displayeventsource}}
{{#subscriptionurl}}

{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}

{{/subscriptionurl}} {{^subscriptionurl}}

{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}

{{/subscriptionurl}}
{{/displayeventsource}} {{/subscription}} PK*\u ADD upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /calendar/* , information provided here is intended especially for developers. === 4.4 === * The following previously deprecated methods have been removed and can no longer be used: - `calendar_process_subscription_row` - `calendar_import_icalendar_events` - `fake_block_threemonths` - `i_click_day_of_this_month_in_calendar` - `i_hover_over_today_in_the_calendar` * The following previously deprecated files have been removed and can no longer be used: - `calendar_threemonth.js` - `calendar_threemonth.mustache` - `threemonth_month.mustache` * The following have been deprecated: - Behat step definition `i_hover_over_today_in_the_calendar` * The event_exporter_base class now returns a field called "branded" indicating whether the module is branded or not. All the external functions using the exporter will now return the new field: - core_calendar_get_action_events_by_course - core_calendar_get_action_events_by_courses - core_calendar_get_action_events_by_timesort - core_calendar_get_calendar_day_view - core_calendar_get_calendar_event_by_id - core_calendar_get_calendar_monthly_view - core_calendar_get_calendar_upcoming_view - core_calendar_submit_create_update_form - core_calendar_update_event_start_day === 4.3 === * The `navigation` property has been removed from `\core_calendar\external\day_exporter` as it is not being used by any of the calendar templates. * The following functions have been deprecated and should no longer be used: - calendar_top_controls() - calendar_get_link_previous() - calendar_get_link_next() === 4.1 === * New method `calendar_format_event_location` which will format the location property of an event, converting any links into suitable markup === 4.0 === * The following external functions now accepts an optional parameter 'searchvalue' to search the events: - core_calendar_external::get_calendar_action_events_by_timesort - core_calendar_external::get_calendar_action_events_by_courses - core_calendar_external::get_calendar_action_events_by_course * Added core_calendar_delete_subscription, which allows to delete the calendar subscription. * Updated calendar_can_manage_user_event() function to check permissions to user events. * The following functions have been deprecated because they were no longer used: - calendar_process_subscription_row() - calendar_import_icalendar_events() * The following has been removed after being deprecated in 3.8 * CALENDAR_EVENT_GLOBAL * The following have been deprecated because the three-month pseudo block has been removed: - fake_block_threemonths() renderer method. - calendar_threemonth.js. - calendar_threemonth and threemonth_month templates. - Behat step definitions i_hover_over_day_of_this_month_in_calendar and i_click_day_of_this_month_in_calendar. === 3.10 === * The core_calendar\local\event\value_objects\times_interface class now has new method get_usermidnight_time() which returns the user midnight time for a given event. === 3.9 === * Plugins can now create their own calendar events, both standard and action ones. To do it they need to specify $event->component when creating an event. Component events can not be edited or deleted manually. See https://moodledev.io/docs/apis/core/calendar#component-events * The following functions have been deprecated because they were no longer used: - calendar_add_event_metadata() - core_calendar_renderer::event() === 3.8 === * The following functions have been finally deprecated and can not be used anymore: * calendar_wday_name() * calendar_get_block_upcoming() * calendar_print_month_selector() * calendar_cron() * calendar_get_mini() * calendar_get_upcoming() * Added core_calendar_external::get_timestamps(), which allows an array containing an arbitrary number of arrays of date/time data to be converted and returned as timestamps, along with an optional key. === 3.6 === * calendar_get_default_courses() function now has optional $userid parameter. * calendar_set_filters() function now has optional $user parameter. * The core_calendar\local\event\container class now provides two new helper methods for getting and setting the requesting user: set_requesting_user() and get_requesting_user(). * The following functions have been finally deprecated and can not be used anymore: * calendar_preferences_button() * added core_calendar_get_valid_event_timestart_range callback for course events when the update_event_start_day function is used in the local api. === 3.5 === * core_calendar_external::get_calendar_events now returns the categoryid for category events. === 3.4 === * calendar_get_mini, and calendar_get_upcoming have been deprecated. Please update to use the new exporters and renderers. * added core_calendar_get_valid_event_timestart_range and core_calendar_event_timestart_updated callbacks for module events when the update_event_start_day function is used in the local api. === 3.3 === * calendar_event_hook() has been removed. Developers should be using the Moodle events system to achieve this behaviour, rather than using a hacky calendar specific implementation. * calendar_wday_name() is deprecated and no longer used in core. * calendar_get_block_upcoming() is deprecated, please use block_calendar_upcoming::get_upcoming_content() instead. * calendar_print_month_selector() is deprecated and no longer used in core. * calendar_cron() is deprecated and should not be used. Please use the core\task\calendar_cron_task instead. === 3.2 === * calendar_preferences_button() is now deprecated. Calendar preferences have been moved to the user preferences page. === 2.9 === default values changes in code: * core_calendar_external::get_calendar_events_parameters() 'timeend' default option changed; now, by default, all events are returned, not only the past ones. * calendar types need to be updated to be compatible with standard PHP date/time code === 2.5 === required changes in code: * calendar_add_icalendar_event() now requires a valid subscriptionid * calendar_process_subscription_row() throws exception for invalid subscriptionid * calendar_update_subscription_events() now throws a dml_exception instead of moodle_exception for bad subscriptions * calendar_get_mini() function now has optional $placement and $courseid paramaters. optional - no changes needed: * calendar_update_subscription() should now be used to update Ical subscriptions. === 2.4 === required changes in code: * calendar_get_popup() function now just returns id, instead of the string "id=$id" PK*\255export_execute.phpnu[. /** * Calendar export * * @package core_calendar * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_MOODLE_COOKIES', true); require_once('../config.php'); require_once($CFG->dirroot.'/calendar/lib.php'); require_once($CFG->libdir.'/bennu/bennu.inc.php'); raise_memory_limit(MEMORY_HUGE); $userid = optional_param('userid', 0, PARAM_INT); $username = optional_param('username', '', PARAM_TEXT); $authtoken = required_param('authtoken', PARAM_ALPHANUM); $generateurl = optional_param('generateurl', '', PARAM_TEXT); if (empty($CFG->enablecalendarexport)) { die('no export'); } $checkuserid = !empty($userid) && $user = \core_user::get_user($userid); // Allowing for fallback check of old url - MDL-27542. $checkusername = !empty($username) && $user = \core_user::get_user_by_username($username); if ((!$checkuserid && !$checkusername) || !$user) { //No such user die('Invalid authentication'); } // Check authentication token. $authuserid = !empty($userid) && $authtoken == calendar_get_export_token($user); // Allowing for fallback check of old url - MDL-27542. $authusername = !empty($username) && $authtoken == sha1($username . $user->password . $CFG->calendar_exportsalt); if (!$authuserid && !$authusername) { die('Invalid authentication'); } // Setup up the user including web access logging. \core\session\manager::set_user($user); $PAGE->set_context(context_system::instance()); // Get the calendar type we are using. $calendartype = \core_calendar\type_factory::get_calendar_instance(); $what = optional_param('preset_what', 'all', PARAM_ALPHA); $time = optional_param('preset_time', 'weeknow', PARAM_ALPHA); $now = $calendartype->timestamp_to_date_array(time()); // Let's see if we have sufficient and correct data $allowedwhat = ['all', 'user', 'groups', 'courses', 'categories']; $allowedtime = ['weeknow', 'weeknext', 'monthnow', 'monthnext', 'recentupcoming', 'custom']; if (!empty($generateurl)) { $authtoken = calendar_get_export_token($user); $params = array(); $params['preset_what'] = $what; $params['preset_time'] = $time; $params['userid'] = $userid; $params['authtoken'] = $authtoken; $params['generateurl'] = true; $link = new moodle_url('/calendar/export.php', $params); redirect($link->out()); die; } $paramcategory = false; if(!empty($what) && !empty($time)) { if(in_array($what, $allowedwhat) && in_array($time, $allowedtime)) { $courses = enrol_get_users_courses($user->id, true, 'id, visible, shortname'); // Array of courses that we will pass to calendar_get_legacy_events() which // is initially set to the list of the user's courses. $paramcourses = $courses; if ($what == 'all' || $what == 'groups') { $groups = array(); foreach ($courses as $course) { $course_groups = groups_get_all_groups($course->id, $user->id); $groups = array_merge($groups, array_keys($course_groups)); } if (empty($groups)) { $groups = false; } } if ($what == 'all') { $users = $user->id; $courses[SITEID] = new stdClass; $courses[SITEID]->shortname = get_string('siteevents', 'calendar'); $paramcourses[SITEID] = $courses[SITEID]; $paramcategory = true; } else if ($what == 'groups') { $users = false; $paramcourses = array(); } else if ($what == 'user') { $users = $user->id; $groups = false; $paramcourses = array(); } else if ($what == 'categories') { $users = $user->id; $groups = false; $paramcourses = array(); $paramcategory = true; } else { $users = false; $groups = false; } // Store the number of days in the week. $numberofdaysinweek = $calendartype->get_num_weekdays(); switch($time) { case 'weeknow': $startweekday = calendar_get_starting_weekday(); $startmonthday = find_day_in_month($now['mday'] - ($numberofdaysinweek - 1), $startweekday, $now['mon'], $now['year']); $startmonth = $now['mon']; $startyear = $now['year']; if ($startmonthday > calendar_days_in_month($startmonth, $startyear)) { [$startmonth, $startyear] = $calendartype->get_next_month($startyear, $startmonth); $startmonthday = find_day_in_month(1, $startweekday, $startmonth, $startyear); } $gregoriandate = $calendartype->convert_to_gregorian($startyear, $startmonth, $startmonthday); $timestart = make_timestamp($gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'], $gregoriandate['hour'], $gregoriandate['minute']); $endmonthday = $startmonthday + $numberofdaysinweek; $endmonth = $startmonth; $endyear = $startyear; if ($endmonthday > calendar_days_in_month($endmonth, $endyear)) { [$endmonth, $endyear] = $calendartype->get_next_month($endyear, $endmonth); $endmonthday = find_day_in_month(1, $startweekday, $endmonth, $endyear); } $gregoriandate = $calendartype->convert_to_gregorian($endyear, $endmonth, $endmonthday); $timeend = make_timestamp($gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'], $gregoriandate['hour'], $gregoriandate['minute']); break; case 'weeknext': $startweekday = calendar_get_starting_weekday(); $startmonthday = find_day_in_month($now['mday'] + 1, $startweekday, $now['mon'], $now['year']); $startmonth = $now['mon']; $startyear = $now['year']; if ($startmonthday > calendar_days_in_month($startmonth, $startyear)) { [$startmonth, $startyear] = $calendartype->get_next_month($startyear, $startmonth); $startmonthday = find_day_in_month(1, $startweekday, $startmonth, $startyear); } $gregoriandate = $calendartype->convert_to_gregorian($startyear, $startmonth, $startmonthday); $timestart = make_timestamp($gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'], $gregoriandate['hour'], $gregoriandate['minute']); $endmonthday = $startmonthday + $numberofdaysinweek; $endmonth = $startmonth; $endyear = $startyear; if ($endmonthday > calendar_days_in_month($endmonth, $endyear)) { [$endmonth, $endyear] = $calendartype->get_next_month($endyear, $endmonth); $endmonthday = find_day_in_month(1, $startweekday, $endmonth, $endyear); } $gregoriandate = $calendartype->convert_to_gregorian($endyear, $endmonth, $endmonthday); $timeend = make_timestamp($gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'], $gregoriandate['hour'], $gregoriandate['minute']); break; case 'monthnow': // Convert to gregorian. $gregoriandate = $calendartype->convert_to_gregorian($now['year'], $now['mon'], 1); $timestart = make_timestamp($gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'], $gregoriandate['hour'], $gregoriandate['minute']); $timeend = $timestart + (calendar_days_in_month($now['mon'], $now['year']) * DAYSECS); break; case 'monthnext': // Get the next month for this calendar. [$nextmonth, $nextyear] = $calendartype->get_next_month($now['year'], $now['mon']); // Convert to gregorian. $gregoriandate = $calendartype->convert_to_gregorian($nextyear, $nextmonth, 1); // Create the timestamps. $timestart = make_timestamp($gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'], $gregoriandate['hour'], $gregoriandate['minute']); $timeend = $timestart + (calendar_days_in_month($nextmonth, $nextyear) * DAYSECS); break; case 'recentupcoming': //Events in the last 5 or next 60 days $timestart = time() - 432000; $timeend = time() + 5184000; break; case 'custom': // Events based on custom date range. $timestart = time() - $CFG->calendar_exportlookback * DAYSECS; $timeend = time() + $CFG->calendar_exportlookahead * DAYSECS; break; } } else { // Parameters given but incorrect, redirect back to export page redirect($CFG->wwwroot.'/calendar/export.php'); die(); } } $limitnum = 0; $events = calendar_get_legacy_events($timestart, $timeend, $users, $groups, array_keys($paramcourses), false, true, $paramcategory, $limitnum); $ical = new iCalendar; $ical->add_property('method', 'PUBLISH'); $ical->add_property('prodid', '-//Moodle Pty Ltd//NONSGML Moodle Version ' . $CFG->version . '//EN'); foreach($events as $event) { if (!empty($event->modulename)) { $instances = get_fast_modinfo($event->courseid, $userid)->get_instances_of($event->modulename); if (empty($instances[$event->instance]->uservisible)) { continue; } } $hostaddress = str_replace('http://', '', $CFG->wwwroot); $hostaddress = str_replace('https://', '', $hostaddress); $me = new calendar_event($event); // To use moodle calendar event services. $ev = new iCalendar_event; // To export in ical format. $ev->add_property('uid', $event->id.'@'.$hostaddress); // Set iCal event summary from event name. $ev->add_property('summary', format_string($event->name, true, ['context' => $me->context])); // Format the description text. $description = format_text($me->description, $me->format, ['context' => $me->context]); // Then convert it to plain text, since it's the only format allowed for the event description property. // We use html_to_text in order to convert
and

tags to new line characters for descriptions in HTML format. $description = html_to_text($description, 0); $ev->add_property('description', $description); $ev->add_property('class', 'PUBLIC'); // PUBLIC / PRIVATE / CONFIDENTIAL $ev->add_property('last-modified', Bennu::timestamp_to_datetime($event->timemodified)); if (!empty($event->location)) { $ev->add_property('location', $event->location); } $ev->add_property('dtstamp', Bennu::timestamp_to_datetime()); // now if ($event->timeduration > 0) { //dtend is better than duration, because it works in Microsoft Outlook and works better in Korganizer $ev->add_property('dtstart', Bennu::timestamp_to_datetime($event->timestart)); // when event starts. $ev->add_property('dtend', Bennu::timestamp_to_datetime($event->timestart + $event->timeduration)); } else if ($event->timeduration == 0) { // When no duration is present, the event is instantaneous event, ex - Due date of a module. // Moodle doesn't support all day events yet. See MDL-56227. $ev->add_property('dtstart', Bennu::timestamp_to_datetime($event->timestart)); $ev->add_property('dtend', Bennu::timestamp_to_datetime($event->timestart)); } else { // This can be used to represent all day events in future. throw new coding_exception("Negative duration is not supported yet."); } if ($event->courseid != 0) { $coursecontext = context_course::instance($event->courseid); $ev->add_property('categories', format_string($courses[$event->courseid]->shortname, true, array('context' => $coursecontext))); } $ical->add_component($ev); } $serialized = $ical->serialize(); if(empty($serialized)) { // TODO die('bad serialization'); } $filename = 'icalexport.ics'; if (!defined('BEHAT_SITE_RUNNING')) { header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT'); header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0'); header('Expires: ' . gmdate('D, d M Y H:i:s', 0) . 'GMT'); header('Pragma: no-cache'); header('Accept-Ranges: none'); // Comment out if PDFs do not work... header('Content-disposition: attachment; filename=' . $filename); header('Content-length: ' . strlen($serialized)); header('Content-type: text/calendar; charset=utf-8'); } echo $serialized; PK*\tt index.phpnu[set_url('/calendar/view.php'); redirect($CFG->wwwroot.'/calendar/view.php');PK*\VQ:dd1type/gregorian/lang/en/calendartype_gregorian.phpnu[. /** * Strings for component 'calendartype_gregorian', language 'en'. * * @package calendartype_gregorian * @copyright 2008 onwards Foodle Group {@link http://foodle.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['name'] = 'Gregorian'; $string['pluginname'] = 'Gregorian calendar type'; $string['privacy:metadata'] = 'The Gregorian calendar format plugin does not store any personal data.'; PK*\ltype/gregorian/version.phpnu[. /** * Version details. * * @package calendartype_gregorian * @copyright 2008 onwards Foodle Group {@link http://foodle.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'calendartype_gregorian'; // Full name of the plugin (used for diagnostics). PK*\P+type/gregorian/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for block_activity_modules. * * @package calendartype_gregorian * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace calendartype_gregorian\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for calendartype_gregorian implementing null_provider. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK*\񒁫44$type/gregorian/classes/structure.phpnu[. namespace calendartype_gregorian; use core_calendar\type_base; /** * Handles calendar functions for the gregorian calendar. * * @package calendartype_gregorian * @copyright 2008 onwards Foodle Group {@link http://foodle.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class structure extends type_base { /** * Returns the name of the calendar. * * This is the non-translated name, usually just * the name of the folder. * * @return string the calendar name */ public function get_name() { return 'gregorian'; } /** * Returns a list of all the possible days for all months. * * This is used to generate the select box for the days * in the date selector elements. Some months contain more days * than others so this function should return all possible days as * we can not predict what month will be chosen (the user * may have JS turned off and we need to support this situation in * Moodle). * * @return array the days */ public function get_days() { $days = array(); for ($i = 1; $i <= 31; $i++) { $days[$i] = $i; } return $days; } /** * Returns a list of all the names of the months. * * @return array the month names */ public function get_months() { $months = array(); $date = new \DateTime('@1263556800'); $date->setTimezone(new \DateTimeZone('UTC')); for ($i = 1; $i <= 12; $i++) { $date->setDate(2000, $i, 15); $months[$i] = userdate($date->getTimestamp(), '%B', 'UTC'); } return $months; } /** * Returns the minimum year for the calendar. * * @return int The minimum year */ public function get_min_year() { return 1900; } /** * Returns the maximum year for the calendar * * @return int The maximum year */ public function get_max_year() { return 2050; } /** * Returns an array of years. * * @param int $minyear * @param int $maxyear * @return array the years */ public function get_years($minyear = null, $maxyear = null) { if (is_null($minyear)) { $minyear = $this->get_min_year(); } if (is_null($maxyear)) { $maxyear = $this->get_max_year(); } $years = array(); for ($i = $minyear; $i <= $maxyear; $i++) { $years[$i] = $i; } return $years; } /** * Returns a multidimensional array with information for day, month, year * and the order they are displayed when selecting a date. * The order in the array will be the order displayed when selecting a date. * Override this function to change the date selector order. * * @param int $minyear The year to start with * @param int $maxyear The year to finish with * @return array Full date information */ public function get_date_order($minyear = null, $maxyear = null) { $dateinfo = array(); $dateinfo['day'] = $this->get_days(); $dateinfo['month'] = $this->get_months(); $dateinfo['year'] = $this->get_years($minyear, $maxyear); return $dateinfo; } /** * Returns the number of days in a week. * * @return int the number of days */ public function get_num_weekdays() { return 7; } /** * Returns an indexed list of all the names of the weekdays. * * The list starts with the index 0. Each index, representing a * day, must be an array that contains the indexes 'shortname' * and 'fullname'. * * @return array array of days */ public function get_weekdays() { return array( 0 => array( 'shortname' => get_string('sun', 'calendar'), 'fullname' => get_string('sunday', 'calendar') ), 1 => array( 'shortname' => get_string('mon', 'calendar'), 'fullname' => get_string('monday', 'calendar') ), 2 => array( 'shortname' => get_string('tue', 'calendar'), 'fullname' => get_string('tuesday', 'calendar') ), 3 => array( 'shortname' => get_string('wed', 'calendar'), 'fullname' => get_string('wednesday', 'calendar') ), 4 => array( 'shortname' => get_string('thu', 'calendar'), 'fullname' => get_string('thursday', 'calendar') ), 5 => array( 'shortname' => get_string('fri', 'calendar'), 'fullname' => get_string('friday', 'calendar') ), 6 => array( 'shortname' => get_string('sat', 'calendar'), 'fullname' => get_string('saturday', 'calendar') ), ); } /** * Returns the index of the starting week day. * * This may vary, for example some may consider Monday as the start of the week, * where as others may consider Sunday the start. * * @return int */ public function get_starting_weekday() { global $CFG; if (isset($CFG->calendar_startwday)) { $firstday = $CFG->calendar_startwday; } else { $firstday = get_string('firstdayofweek', 'langconfig'); } if (!is_numeric($firstday)) { $startingweekday = CALENDAR_DEFAULT_STARTING_WEEKDAY; } else { $startingweekday = intval($firstday) % 7; } return get_user_preferences('calendar_startwday', $startingweekday); } /** * Returns the index of the weekday for a specific calendar date. * * @param int $year * @param int $month * @param int $day * @return int */ public function get_weekday($year, $month, $day) { return intval(date('w', mktime(12, 0, 0, $month, $day, $year))); } /** * Returns the number of days in a given month. * * @param int $year * @param int $month * @return int the number of days */ public function get_num_days_in_month($year, $month) { return intval(date('t', mktime(0, 0, 0, $month, 1, $year))); } /** * Get the previous month. * * If the current month is January, it will get the last month of the previous year. * * @param int $year * @param int $month * @return array previous month and year */ public function get_prev_month($year, $month) { if ($month == 1) { return array(12, $year - 1); } else { return array($month - 1, $year); } } /** * Get the next month. * * If the current month is December, it will get the first month of the following year. * * @param int $year * @param int $month * @return array the following month and year */ public function get_next_month($year, $month) { if ($month == 12) { return array(1, $year + 1); } else { return array($month + 1, $year); } } /** * Returns a formatted string that represents a date in user time. * * If parameter fixday = true (default), then take off leading * zero from %d, else maintain it. * * @param int $time the timestamp in UTC, as obtained from the database * @param string $format strftime format * @param int|float|string $timezone the timezone to use * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} * @param bool $fixday if true then the leading zero from %d is removed, * if false then the leading zero is maintained * @param bool $fixhour if true then the leading zero from %I is removed, * if false then the leading zero is maintained * @return string the formatted date/time */ public function timestamp_to_date_string($time, $format, $timezone, $fixday, $fixhour) { global $CFG; if (empty($format)) { $format = get_string('strftimedaydatetime', 'langconfig'); } // Note: This historical logic was about fixing 12-hour time to remove // unnecessary leading zero was required because on Windows, PHP strftime // function did not support the correct 'hour without leading zero' parameter (%l). // This is no longer required because we use IntlDateFormatter. // Unfortunately though the original implementation was done incorrectly. // The documentation for strftime notes that for the "%l" and "%e" specifiers where // no leading zero is used, a space is used instead. // As a result we switch to the new format specifiers "%l" and "%e", wrap them in placeholders // and then remove the spaces. if (empty($CFG->nofixday) && $fixday) { // Config.php can force %d not to be fixed, but only if the format did not specify it. $format = str_replace( '%d', 'DDHH%eHHDD', $format, ); } if (empty($CFG->nofixhour) && $fixhour) { $format = str_replace( '%I', 'DDHH%lHHDD', $format, ); } if (is_string($time) && !is_numeric($time)) { debugging( "Invalid time passed to timestamp_to_date_string: '{$time}'", DEBUG_DEVELOPER, ); $time = 0; } if ($time === null || $time === '') { $time = 0; } $time = new \DateTime("@{$time}", new \DateTimeZone(date_default_timezone_get())); date_default_timezone_set(\core_date::get_user_timezone($timezone)); $formattedtime = \core_date::strftime( $format, $time, get_string('locale', 'langconfig'), ); \core_date::set_default_server_timezone(); // Use a simple regex to remove the placeholders and any leading spaces to match the historically // generated format. $formattedtime = preg_replace('/DDHH ?(\d{1,2})HHDD/', '$1', $formattedtime); return $formattedtime; } /** * Given a $time timestamp in GMT (seconds since epoch), returns an array that * represents the date in user time. * * @param int $time Timestamp in GMT * @param float|int|string $timezone offset's time with timezone, if float and not 99, then no * dst offset is applied {@link https://moodledev.io/docs/apis/subsystems/time#timezone} * @return array an array that represents the date in user time */ public function timestamp_to_date_array($time, $timezone = 99) { return usergetdate($time, $timezone); } /** * Provided with a day, month, year, hour and minute in a specific * calendar type convert it into the equivalent Gregorian date. * * In this function we don't need to do anything except pass the data * back as an array. This is because the date received is Gregorian. * * @param int $year * @param int $month * @param int $day * @param int $hour * @param int $minute * @return array the converted date */ public function convert_from_gregorian($year, $month, $day, $hour = 0, $minute = 0) { $date = array(); $date['year'] = $year; $date['month'] = $month; $date['day'] = $day; $date['hour'] = $hour; $date['minute'] = $minute; return $date; } /** * Provided with a day, month, year, hour and minute in a specific * calendar type convert it into the equivalent Gregorian date. * * In this function we don't need to do anything except pass the data * back as an array. This is because the date received is Gregorian. * * @param int $year * @param int $month * @param int $day * @param int $hour * @param int $minute * @return array the converted date */ public function convert_to_gregorian($year, $month, $day, $hour = 0, $minute = 0) { $date = array(); $date['year'] = $year; $date['month'] = $month; $date['day'] = $day; $date['hour'] = $hour; $date['minute'] = $minute; return $date; } /** * This return locale for windows os. * * @return string locale */ public function locale_win_charset() { return get_string('localewincharset', 'langconfig'); } } PK*\l5t88'type/gregorian/tests/structure_test.phpnu[. namespace calendartype_gregorian; /** * Tests for Gregorian calendar type * * @package calendartype_gregorian * @category test * @copyright Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \calendartype_gregorian\structure */ final class structure_test extends \advanced_testcase { public function tearDown(): void { parent::tearDown(); get_string_manager(true); } /** * Test the timestamp_to_date_string method with different input values. * * @dataProvider timestamp_to_date_string_provider * @param string $locale * @param int $timestamp * @param string $format * @param string $timezone * @param bool $fixday * @param bool $fixhour * @param string $expected */ public function test_timestamp_to_date_string( string $locale, int $timestamp, string $format, string $timezone, bool $fixday, bool $fixhour, string $expected, ): void { $this->resetAfterTest(); $stringmanager = $this->get_mocked_string_manager(); $stringmanager->mock_string('locale', 'langconfig', $locale); $structure = new structure(); $this->assertEquals( $expected, $structure->timestamp_to_date_string( $timestamp, $format, $timezone, $fixday, $fixhour, ), ); } /** * Data provider for timestamp_to_date_string tests. * * @return array */ public static function timestamp_to_date_string_provider(): array { return [ 'English with UTC timezone' => [ 'en', 0, '%Y-%m-%d %H:%M:%S', 'UTC', false, false, '1970-01-01 00:00:00', ], 'English with London timezone' => [ 'en', 1728487003, "%d %B %Y", 'Europe/London', false, false, "09 October 2024", ], 'English with Sydney (+11) timezone' => [ 'en', 1728487003, "%d %B %Y", 'Australia/Sydney', false, false, "10 October 2024", ], 'Russian with Sydney (+11) timezone' => [ 'ru', 1728487003, "%d %B %Y %H:%M:%S", 'Australia/Sydney', false, false, '10 октября 2024 02:16:43', ], 'Russian %B %Y (Genitive) with Sydney (+11) timezone' => [ 'ru', 1728487003, "%B %Y", 'Australia/Sydney', false, false, "октябрь 2024", ], 'Russian %d %B %Y (Nominative) with London timezone' => [ 'ru', 1728487003, "%d %B %Y", 'Europe/London', false, false, "09 октября 2024", ], 'Russian %d %B %Y (Nominative) with London timezone fixing leading zero' => [ 'ru', 1728487003, "%d %B %Y", 'Europe/London', true, false, "9 октября 2024", ], 'Russian %e %B %Y (Nominative) with London timezone' => [ 'ru', 1728487003, "%e %B %Y", 'Europe/London', false, false, " 9 октября 2024", ], 'Time %I without fixing leading zero' => [ 'ru', 1728487003, "%I:%M:%S", 'Australia/Sydney', false, false , "02:16:43", ], 'Time %I fixing leading zero' => [ 'ru', 1728487003, "%I:%M:%S", 'Australia/Sydney', false, true , "2:16:43", ], 'Time %l without fixing leading zero' => [ 'ru', 1728487003, "%l:%M:%S", 'Australia/Sydney', false, false, " 2:16:43", ], 'Time %l fixing leading zero' => [ 'ru', 1728487003, "%l:%M:%S", 'Australia/Sydney', false, true, " 2:16:43", ], ]; } } PK*\Eޒ0 import.phpnu[. /** * Moodle calendar import * * @package core_calendar * @copyright Moodle Pty Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once($CFG->libdir . '/bennu/bennu.inc.php'); require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->dirroot . '/calendar/lib.php'); $courseid = optional_param('course', SITEID, PARAM_INT); $groupcourseid = optional_param('groupcourseid', 0, PARAM_INT); $category = optional_param('category', 0, PARAM_INT); $data = []; $pageurl = new moodle_url('/calendar/import.php'); $managesubscriptionsurl = new moodle_url('/calendar/managesubscriptions.php'); $headingstr = $calendarstr = get_string('calendar', 'calendar'); if (!empty($courseid) && $courseid != SITEID) { $course = get_course($courseid); $data['eventtype'] = 'course'; $data['courseid'] = $course->id; $pageurl->param('course', $course->id); $managesubscriptionsurl->param('course', $course->id); $headingstr .= ": {$course->shortname}"; navigation_node::override_active_url(new moodle_url('/course/view.php', ['id' => $course->id])); $PAGE->navbar->add( $calendarstr, new moodle_url('/calendar/view.php', ['view' => 'month', 'course' => $course->id]) ); } else if (!empty($category)) { $course = get_site(); $pageurl->param('category', $category); $managesubscriptionsurl->param('category', $category); $data['category'] = $category; $data['eventtype'] = 'category'; navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $category])); $PAGE->set_category_by_id($category); $PAGE->navbar->add( $calendarstr, new moodle_url('/calendar/view.php', ['view' => 'month', 'category' => $category]) ); } else { $course = get_site(); $PAGE->navbar->add($calendarstr, new moodle_url('/calendar/view.php', ['view' => 'month'])); } if (!empty($groupcourseid)) { $pageurl->param('groupcourseid', $groupcourseid); } require_login($course, false); if (!calendar_user_can_add_event($course)) { throw new \moodle_exception('errorcannotimport', 'calendar'); } $heading = get_string('importcalendar', 'calendar'); $pagetitle = $course->shortname . ': ' . $calendarstr . ': ' . $heading; $PAGE->set_secondary_navigation(false); $PAGE->set_title($pagetitle); $PAGE->set_heading($headingstr); $PAGE->set_url($pageurl); $PAGE->set_pagelayout('admin'); $PAGE->navbar->add(get_string('managesubscriptions', 'calendar'), $managesubscriptionsurl); $PAGE->navbar->add($heading, $pageurl); // Populate the 'group' select box based on the given 'groupcourseid', if necessary. $groups = []; if (!empty($groupcourseid)) { require_once($CFG->libdir . '/grouplib.php'); $groupcoursedata = groups_get_course_data($groupcourseid); if (!empty($groupcoursedata->groups)) { foreach ($groupcoursedata->groups as $groupid => $groupdata) { $groups[$groupid] = $groupdata->name; } } $data['groupcourseid'] = $groupcourseid; $data['eventtype'] = 'group'; } if (!empty($category)) { $managesubscriptionsurl->param('category', $category); $data['category'] = $category; $data['eventtype'] = 'category'; } $renderer = $PAGE->get_renderer('core_calendar'); $customdata = [ 'courseid' => $course->id, 'groups' => $groups, ]; $form = new \core_calendar\local\event\forms\managesubscriptions(null, $customdata); $form->set_data($data); $formdata = $form->get_data(); if (!empty($formdata)) { require_sesskey(); $subscriptionid = calendar_add_subscription($formdata); if ($formdata->importfrom == CALENDAR_IMPORT_FROM_FILE) { // Blank the URL if it's a file import. $formdata->url = ''; $calendar = $form->get_file_content('importfile'); $ical = new iCalendar(); $ical->unserialize($calendar); $importresults = calendar_import_events_from_ical($ical, $subscriptionid); } else { try { $importresults = calendar_update_subscription_events($subscriptionid); } catch (\moodle_exception $e) { // Delete newly added subscription and show invalid url error. calendar_delete_subscription($subscriptionid); throw new \moodle_exception($e->errorcode, $e->module, $PAGE->url); } } if (!empty($formdata->courseid)) { $managesubscriptionsurl->param('course', $formdata->courseid); } if (!empty($formdata->categoryid)) { $managesubscriptionsurl->param('category', $formdata->categoryid); } redirect($managesubscriptionsurl, $renderer->render_import_result($importresults)); } echo $OUTPUT->header(); echo $renderer->start_layout(); echo $OUTPUT->heading($heading); $form->display(); echo $renderer->complete_layout(); echo $OUTPUT->footer(); PK*\9managesubscriptions.phpnu[. /** * Allows the user to manage calendar subscriptions. * * @copyright 2012 Jonathan Harker * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package calendar */ require_once('../config.php'); require_once($CFG->libdir.'/bennu/bennu.inc.php'); require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); // Required use. $courseid = optional_param('course', null, PARAM_INT); $categoryid = optional_param('category', null, PARAM_INT); $url = new moodle_url('/calendar/managesubscriptions.php'); if ($courseid != SITEID && !empty($courseid)) { $url->param('course', $courseid); navigation_node::override_active_url(new moodle_url('/course/view.php', ['id' => $courseid])); $PAGE->navbar->add( get_string('calendar', 'calendar'), new moodle_url('/calendar/view.php', ['view' => 'month', 'course' => $courseid]) ); } else if ($categoryid) { $url->param('categoryid', $categoryid); navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $categoryid])); $PAGE->set_category_by_id($categoryid); $PAGE->navbar->add( get_string('calendar', 'calendar'), new moodle_url('/calendar/view.php', ['view' => 'month', 'category' => $categoryid]) ); } else { $PAGE->navbar->add(get_string('calendar', 'calendar'), new moodle_url('/calendar/view.php', ['view' => 'month'])); } $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); $PAGE->set_secondary_navigation(false); if ($courseid != SITEID && !empty($courseid)) { // Course ID must be valid and existing. $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); $courses = array($course->id => $course); } else { $course = get_site(); $courses = calendar_get_default_courses(); } require_login($course, false); if (!calendar_user_can_add_event($course)) { throw new \moodle_exception('errorcannotimport', 'calendar'); } $PAGE->navbar->add(get_string('managesubscriptions', 'calendar'), $PAGE->url); $types = calendar_get_allowed_event_types($courseid); $searches = []; $params = []; $usedefaultfilters = true; if (!empty($types['site'])) { $searches[] = "(eventtype = 'site')"; $usedefaultfilters = false; } if (!empty($types['user'])) { $searches[] = "(eventtype = 'user' AND userid = :userid)"; $params['userid'] = $USER->id; $usedefaultfilters = false; } if (!empty($courseid) && !empty($types['course'])) { $searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid = :courseid)"; $params += ['courseid' => $courseid]; $usedefaultfilters = false; } if (!empty($types['category'])) { if (!empty($categoryid)) { $searches[] = "(eventtype = 'category' AND categoryid = :categoryid)"; $params += ['categoryid' => $categoryid]; } else { $searches[] = "(eventtype = 'category')"; } $usedefaultfilters = false; } if ($usedefaultfilters) { $searches[] = "(eventtype = 'user' AND userid = :userid)"; $params['userid'] = $USER->id; if (!empty($types['site'])) { $searches[] = "(eventtype = 'site' AND courseid = :siteid)"; $params += ['siteid' => SITEID]; } if (!empty($types['course'])) { $courses = calendar_get_default_courses(null, 'id', true); if (!empty($courses)) { $courseids = array_map(function ($c) { return $c->id; }, $courses); list($courseinsql, $courseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED, 'course'); $searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid {$courseinsql})"; $params += $courseparams; } } if (!empty($types['category'])) { list($categoryinsql, $categoryparams) = $DB->get_in_or_equal( array_keys(\core_course_category::make_categories_list('moodle/category:manage')), SQL_PARAMS_NAMED, 'category'); $searches[] = "(eventtype = 'category' AND categoryid {$categoryinsql})"; $params += $categoryparams; } } $sql = "SELECT * FROM {event_subscriptions} WHERE " . implode(' OR ', $searches);; $subscriptions = $DB->get_records_sql($sql, $params); // Print title and header. $PAGE->set_title("$course->shortname: ".get_string('calendar', 'calendar').": ".get_string('subscriptions', 'calendar')); $heading = get_string('calendar', 'core_calendar'); $heading = ($courseid != SITEID && !empty($courseid)) ? "{$heading}: {$COURSE->shortname}" : $heading; $PAGE->set_heading($heading); $renderer = $PAGE->get_renderer('core_calendar'); echo $OUTPUT->header(); echo $renderer->render_subscriptions_header(); // Filter subscriptions which user can't edit. foreach($subscriptions as $subscription) { if (!calendar_can_edit_subscription($subscription)) { unset($subscriptions[$subscription->id]); } } // Display a table of subscriptions. if (empty($subscription)) { echo $renderer->render_no_calendar_subscriptions(); } else { echo $renderer->subscription_details($courseid, $subscriptions); } echo $OUTPUT->footer(); PK*\D>z4z4amd/src/modal_event_form.jsnu[// 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 . /** * Contain the logic for the quick add or update event modal. * * @module core_calendar/modal_event_form * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import * as CustomEvents from 'core/custom_interaction_events'; import Modal from 'core/modal'; import * as FormEvents from 'core_form/events'; import CalendarEvents from './events'; import * as Str from 'core/str'; import * as Notification from 'core/notification'; import * as Fragment from 'core/fragment'; import * as Repository from 'core_calendar/repository'; const SELECTORS = { SAVE_BUTTON: '[data-action="save"]', LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]', }; export default class ModalEventForm extends Modal { static TYPE = 'core_calendar-modal_event_form'; static TEMPLATE = 'calendar/modal_event_form'; /** * Constructor for the Modal. * * @param {object} root The root jQuery element for the modal */ constructor(root) { super(root); 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); } configure(modalConfig) { modalConfig.large = true; super.configure(modalConfig); } /** * Set the context id to the given value. * * @method setContextId * @param {Number} id The event id */ setContextId(id) { this.contextId = id; } /** * Retrieve the current context id, if any. * * @method getContextId * @return {Number|null} The event id */ getContextId() { return this.contextId; } /** * Set the course id to the given value. * * @method setCourseId * @param {Number} id The event id */ setCourseId(id) { this.courseId = id; } /** * Retrieve the current course id, if any. * * @method getCourseId * @return {Number|null} The event id */ getCourseId() { return this.courseId; } /** * Set the category id to the given value. * * @method setCategoryId * @param {Number} id The event id */ setCategoryId(id) { this.categoryId = id; } /** * Retrieve the current category id, if any. * * @method getCategoryId * @return {Number|null} The event id */ getCategoryId() { return this.categoryId; } /** * Check if the modal has an course id. * * @method hasCourseId * @return {bool} */ hasCourseId() { return this.courseId !== null; } /** * Check if the modal has an category id. * * @method hasCategoryId * @return {bool} */ hasCategoryId() { return this.categoryId !== null; } /** * Set the event id to the given value. * * @method setEventId * @param {Number} id The event id */ setEventId(id) { this.eventId = id; } /** * Retrieve the current event id, if any. * * @method getEventId * @return {Number|null} The event id */ getEventId() { return this.eventId; } /** * Check if the modal has an event id. * * @method hasEventId * @return {bool} */ hasEventId() { return this.eventId !== null; } /** * Set the start time to the given value. * * @method setStartTime * @param {Number} time The start time */ setStartTime(time) { this.startTime = time; } /** * Retrieve the current start time, if any. * * @method getStartTime * @return {Number|null} The start time */ getStartTime() { return this.startTime; } /** * Check if the modal has start time. * * @method hasStartTime * @return {bool} */ hasStartTime() { return this.startTime !== null; } /** * Get the form element from the modal. * * @method getForm * @return {object} */ getForm() { return this.getBody().find('form'); } /** * Disable the buttons in the footer. * * @method disableButtons */ disableButtons() { this.saveButton.prop('disabled', true); } /** * Enable the buttons in the footer. * * @method enableButtons */ enableButtons() { this.saveButton.prop('disabled', false); } /** * Reload the title for the modal to the appropriate value * depending on whether we are creating a new event or * editing an existing event. * * @method reloadTitleContent * @return {object} A promise resolved with the new title text */ reloadTitleContent() { if (this.reloadingTitle) { return this.titlePromise; } this.reloadingTitle = true; if (this.hasEventId()) { this.titlePromise = Str.get_string('editevent', 'calendar'); } else { this.titlePromise = Str.get_string('newevent', 'calendar'); } this.titlePromise.then((string) => { this.setTitle(string); return string; }) .catch(Notification.exception) .always(() => { this.reloadingTitle = false; return; }); return this.titlePromise; } /** * Send a request to the server to get the event_form in a fragment * and render the result in the body of the modal. * * If serialised form data is provided then it will be sent in the * request to the server to have the form rendered with the data. This * is used when the form had a server side error and we need the server * to re-render it for us to display the error to the user. * * @method reloadBodyContent * @param {string} formData The serialised form data * @return {object} A promise resolved with the fragment html and js from */ reloadBodyContent(formData) { if (this.reloadingBody) { return this.bodyPromise; } this.reloadingBody = true; this.disableButtons(); const args = {}; if (this.hasEventId()) { args.eventid = this.getEventId(); } if (this.hasStartTime()) { args.starttime = this.getStartTime(); } if (this.hasCourseId()) { 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', this.getContextId(), args); this.setBody(this.bodyPromise); this.bodyPromise.then(() => { this.enableButtons(); return; }) .catch(Notification.exception) .always(() => { this.reloadingBody = false; return; }); return this.bodyPromise; } /** * Reload both the title and body content. * * @method reloadAllContent * @return {object} promise */ reloadAllContent() { return $.when(this.reloadTitleContent(), this.reloadBodyContent()); } /** * Kick off a reload the modal content before showing it. This * is to allow us to re-use the same modal for creating and * editing different events within the page. * * We do the reload when showing the modal rather than hiding it * to save a request to the server if the user closes the modal * and never re-opens it. * * @method show */ show() { this.reloadAllContent(); super.show(this); } /** * Clear the event id from the modal when it's closed so * that it is loaded fresh next time it's displayed. * * The event id will be set by the calling code if it wants * to edit a specific event. * * @method hide */ hide() { super.hide(this); this.setEventId(null); this.setStartTime(null); this.setCourseId(null); this.setCategoryId(null); } /** * Get the serialised form data. * * @method getFormData * @return {string} serialised form data */ getFormData() { return this.getForm().serialize(); } /** * Send the form data to the server to create or update * an event. * * If there is a server side validation error then we re-request the * rendered form (with the data) from the server in order to get the * server side errors to display. * * On success the modal is hidden and the page is reloaded so that the * new event will display. * * @method save * @return {object} A promise */ save() { const loadingContainer = this.saveButton.find(SELECTORS.LOADING_ICON_CONTAINER); // Now the change events have run, see if there are any "invalid" form fields. const invalid = this.getForm().find('[aria-invalid="true"]'); // If we found invalid fields, focus on the first one and do not submit via ajax. if (invalid.length) { invalid.first().focus(); return Promise.resolve(); } loadingContainer.removeClass('hidden'); this.disableButtons(); const formData = this.getFormData(); // Send the form data to the server for processing. return Repository.submitCreateUpdateForm(formData) .then((response) => { if (response.validationerror) { // 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. this.reloadBodyContent(formData); return; } else { // Check whether this was a new event or not. // The hide function unsets the form data so grab this before the hide. const isExisting = this.hasEventId(); // No problemo! Our work here is done. this.hide(); // Trigger the appropriate calendar event so that the view can be updated. if (isExisting) { $('body').trigger(CalendarEvents.updated, [response.event]); } else { $('body').trigger(CalendarEvents.created, [response.event]); } } return; }) .catch(Notification.exception) .always(() => { // Regardless of success or error we should always stop // the loading icon and re-enable the buttons. loadingContainer.addClass('hidden'); this.enableButtons(); return; }); } /** * Set up all of the event handling for the modal. * * @method registerEventListeners * @fires event:uploadStarted * @fires event:formSubmittedByJavascript */ registerEventListeners() { // Apply parent event listeners. super.registerEventListeners(this); // When the user clicks the save button we trigger the form submission. We need to // trigger an actual submission because there is some JS code in the form that is // listening for this event and doing some stuff (e.g. saving draft areas etc). this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, (e, data) => { this.getForm().submit(); data.originalEvent.preventDefault(); e.stopPropagation(); }); // Catch the submit event before it is actually processed by the browser and // prevent the submission. We'll take it from here. this.getModal().on('submit', (e) => { FormEvents.notifyFormSubmittedByJavascript(this.getForm()[0]); this.save(); // Stop the form from actually submitting and prevent it's // propagation because we have already handled the event. e.preventDefault(); e.stopPropagation(); }); } } ModalEventForm.registerModalType(); PK*\E֬Z#Z#amd/src/crud.jsnu[// 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 . /** * A module to handle CRUD operations within the UI. * * @module core_calendar/crud * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/str', 'core/notification', 'core/modal_events', 'core_calendar/modal_event_form', 'core_calendar/repository', 'core_calendar/events', 'core_calendar/modal_delete', 'core_calendar/selectors', 'core/pending', 'core/modal_save_cancel', 'core/config', ], function( $, Str, Notification, ModalEvents, ModalEventForm, CalendarRepository, CalendarEvents, CalendarModalDelete, CalendarSelectors, Pending, ModalSaveCancel, Config, ) { /** * 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 pendingPromise = new Pending('core_calendar/crud:confirmDeletion'); 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 = CalendarModalDelete.create(); } else { deleteStrings.push({ key: 'confirmeventdelete', component: 'calendar', param: eventTitle }); deletePromise = ModalSaveCancel.create(); } var stringsPromise = Str.get_strings(deleteStrings); var finalPromise = $.when(stringsPromise, deletePromise) .then(function(strings, deleteModal) { deleteModal.setRemoveOnClose(true); deleteModal.setTitle(strings[0]); deleteModal.setBody(strings[1]); if (!isRepeatedEvent) { deleteModal.setSaveButtonText(strings[0]); } deleteModal.show(); deleteModal.getRoot().on(ModalEvents.save, function() { var pendingPromise = new Pending('calendar/crud:initModal:deletedevent'); CalendarRepository.deleteEvent(eventId, false) .then(function() { $('body').trigger(CalendarEvents.deleted, [eventId, false]); return; }) .then(pendingPromise.resolve) .catch(Notification.exception); }); deleteModal.getRoot().on(CalendarEvents.deleteAll, function() { var pendingPromise = new Pending('calendar/crud:initModal:deletedallevent'); CalendarRepository.deleteEvent(eventId, true) .then(function() { $('body').trigger(CalendarEvents.deleted, [eventId, true]); return; }) .then(pendingPromise.resolve) .catch(Notification.exception); }); return deleteModal; }) .then(function(modal) { pendingPromise.resolve(); return modal; }) .catch(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 = ModalEventForm.create(); // 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'); const courseId = wrapper.data('courseid'); if (typeof categoryId !== 'undefined' && courseId != Config.siteId) { 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; }) .catch(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.setCourseId(eventWrapper.data('courseId')); modal.show(); e.stopImmediatePropagation(); return; }).catch(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(); }); } /** * Register the listeners required to edit the event. * * @param {jQuery} root * @param {Promise} eventFormModalPromise * @returns {Promise} */ function registerEditListeners(root, eventFormModalPromise) { var pendingPromise = new Pending('core_calendar/crud:registerEditListeners'); return 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 target = root.find(`[data-event-id=${eventId}]`), calendarWrapper = root.find(CalendarSelectors.wrapper); modal.setEventId(eventId); modal.setContextId(calendarWrapper.data('contextId')); modal.setReturnElement(target); modal.show(); e.stopImmediatePropagation(); }); return modal; }) .then(function(modal) { pendingPromise.resolve(); return modal; }) .catch(Notification.exception); } return { registerRemove: registerRemove, registerEditListeners: registerEditListeners, registerEventFormModal: registerEventFormModal }; }); PK*\[Z,4,4amd/src/month_view_drag_drop.jsnu[// 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 . /** * A javascript module to handle calendar drag and drop in the calendar * month view. * * @module core_calendar/month_view_drag_drop * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/notification', 'core/str', 'core_calendar/events', 'core_calendar/drag_drop_data_store' ], function( $, Notification, Str, CalendarEvents, DataStore ) { var SELECTORS = { ROOT: "[data-region='calendar']", DRAGGABLE: '[draggable="true"][data-region="event-item"]', DROP_ZONE: '[data-drop-zone="month-view-day"]', WEEK: '[data-region="month-view-week"]', }; var INVALID_DROP_ZONE_CLASS = 'bg-faded'; var INVALID_HOVER_CLASS = 'bg-danger text-white'; var VALID_HOVER_CLASS = 'bg-primary text-white'; var ALL_CLASSES = INVALID_DROP_ZONE_CLASS + ' ' + INVALID_HOVER_CLASS + ' ' + VALID_HOVER_CLASS; /* @var {bool} registered If the event listeners have been added */ var registered = false; /** * Get the correct drop zone element from the given javascript * event. * * @param {event} e The javascript event * @return {object|null} */ var getDropZoneFromEvent = function(e) { var dropZone = $(e.target).closest(SELECTORS.DROP_ZONE); return (dropZone.length) ? dropZone : null; }; /** * Determine if the given dropzone element is within the acceptable * time range. * * The drop zone timestamp is midnight on that day so we should check * that the event's acceptable timestart value * * @param {object} dropZone The drop zone day from the calendar * @return {bool} */ var isValidDropZone = function(dropZone) { var dropTimestamp = dropZone.attr('data-day-timestamp'); var minTimestart = DataStore.getMinTimestart(); var maxTimestart = DataStore.getMaxTimestart(); if (minTimestart && minTimestart > dropTimestamp) { return false; } if (maxTimestart && maxTimestart < dropTimestamp) { return false; } return true; }; /** * Get the error string to display for a given drop zone element * if it is invalid. * * @param {object} dropZone The drop zone day from the calendar * @return {string} */ var getDropZoneError = function(dropZone) { var dropTimestamp = dropZone.attr('data-day-timestamp'); var minTimestart = DataStore.getMinTimestart(); var maxTimestart = DataStore.getMaxTimestart(); if (minTimestart && minTimestart > dropTimestamp) { return DataStore.getMinError(); } if (maxTimestart && maxTimestart < dropTimestamp) { return DataStore.getMaxError(); } return null; }; /** * Remove all of the styling from each of the drop zones in the calendar. */ var clearAllDropZonesState = function() { $(SELECTORS.ROOT).find(SELECTORS.DROP_ZONE).each(function(index, dropZone) { dropZone = $(dropZone); dropZone.removeClass(ALL_CLASSES); }); }; /** * Update the hover state for the event in the calendar to reflect * which days the event will be moved to. * * If the drop zone is not being hovered then it will apply some * styling to reflect whether the drop zone is a valid or invalid * drop place for the current dragging event. * * This funciton supports events spanning multiple days and will * recurse to highlight (or remove highlight) each of the days * that the event will be moved to. * * For example: An event with a duration of 3 days will have * 3 days highlighted when it's dragged elsewhere in the calendar. * The current drag target and the 2 days following it (including * wrapping to the next week if necessary). * * @param {string|object} dropZone The drag target element * @param {bool} hovered If the target is hovered or not * @param {Number} count How many days to highlight (default to duration) */ var updateHoverState = function(dropZone, hovered, count) { if (typeof count === 'undefined') { // This is how many days we need to highlight. count = DataStore.getDurationDays(); } var valid = isValidDropZone(dropZone); dropZone.removeClass(ALL_CLASSES); if (hovered) { if (valid) { dropZone.addClass(VALID_HOVER_CLASS); } else { dropZone.addClass(INVALID_HOVER_CLASS); } } else { dropZone.removeClass(VALID_HOVER_CLASS + ' ' + INVALID_HOVER_CLASS); if (!valid) { dropZone.addClass(INVALID_DROP_ZONE_CLASS); } } count--; // If we've still got days to highlight then we should // find the next day. if (count > 0) { var nextDropZone = dropZone.next(); // If there are no more days in this week then we // need to move down to the next week in the calendar. if (!nextDropZone.length) { var nextWeek = dropZone.closest(SELECTORS.WEEK).next(); if (nextWeek.length) { nextDropZone = nextWeek.children(SELECTORS.DROP_ZONE).first(); } } // If we found another day then let's recursively // update it's hover state. if (nextDropZone.length) { updateHoverState(nextDropZone, hovered, count); } } }; /** * Find all of the calendar event drop zones in the calendar and update the display * for the user to indicate which zones are valid and invalid. */ var updateAllDropZonesState = function() { $(SELECTORS.ROOT).find(SELECTORS.DROP_ZONE).each(function(index, dropZone) { dropZone = $(dropZone); if (!isValidDropZone(dropZone)) { updateHoverState(dropZone, false); } }); }; /** * Set up the module level variables to track which event is being * dragged and how many days it spans. * * @param {event} e The dragstart event */ var dragstartHandler = function(e) { var target = $(e.target); var draggableElement = target.closest(SELECTORS.DRAGGABLE); if (!draggableElement.length) { return; } var eventElement = draggableElement.find('[data-event-id]'); var eventId = eventElement.attr('data-event-id'); var minTimestart = draggableElement.attr('data-min-day-timestamp'); var maxTimestart = draggableElement.attr('data-max-day-timestamp'); var minError = draggableElement.attr('data-min-day-error'); var maxError = draggableElement.attr('data-max-day-error'); var eventsSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]'; var duration = $(eventsSelector).length; DataStore.setEventId(eventId); DataStore.setDurationDays(duration); if (minTimestart) { DataStore.setMinTimestart(minTimestart); } if (maxTimestart) { DataStore.setMaxTimestart(maxTimestart); } if (minError) { DataStore.setMinError(minError); } if (maxError) { DataStore.setMaxError(maxError); } e.dataTransfer.effectAllowed = "move"; e.dataTransfer.dropEffect = "move"; // Firefox requires a value to be set here or the drag won't // work and the dragover handler won't fire. e.dataTransfer.setData('text/plain', eventId); e.dropEffect = "move"; updateAllDropZonesState(); }; /** * Update the hover state of the target day element when * the user is dragging an event over it. * * This will add a visual indicator to the calendar UI to * indicate which day(s) the event will be moved to. * * @param {event} e The dragstart event */ var dragoverHandler = function(e) { // Ignore dragging of non calendar events. if (!DataStore.hasEventId()) { return; } e.preventDefault(); var dropZone = getDropZoneFromEvent(e); if (!dropZone) { return; } updateHoverState(dropZone, true); }; /** * Update the hover state of the target day element that was * previously dragged over but has is no longer a drag target. * * This will remove the visual indicator from the calendar UI * that was added by the dragoverHandler. * * @param {event} e The dragstart event */ var dragleaveHandler = function(e) { // Ignore dragging of non calendar events. if (!DataStore.hasEventId()) { return; } var dropZone = getDropZoneFromEvent(e); if (!dropZone) { return; } updateHoverState(dropZone, false); e.preventDefault(); }; /** * Determines the event element, origin day, and destination day * once the user drops the calendar event. These three bits of data * are provided as the payload to the "moveEvent" calendar javascript * event that is fired. * * This will remove the visual indicator from the calendar UI * that was added by the dragoverHandler. * * @param {event} e The dragstart event */ var dropHandler = function(e) { // Ignore dragging of non calendar events. if (!DataStore.hasEventId()) { return; } var dropZone = getDropZoneFromEvent(e); if (!dropZone) { DataStore.clearAll(); clearAllDropZonesState(); return; } if (isValidDropZone(dropZone)) { var eventId = DataStore.getEventId(); var eventElementSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]'; var eventElement = $(eventElementSelector); var origin = null; if (eventElement.length) { origin = eventElement.closest(SELECTORS.DROP_ZONE); } $('body').trigger(CalendarEvents.moveEvent, [eventId, origin, dropZone]); } else { // If the drop zone is not valid then there is not need for us to // try to process it. Instead we can just show an error to the user. var message = getDropZoneError(dropZone); Str.get_string('errorinvaliddate', 'calendar').then(function(string) { Notification.exception({ name: string, message: message || string }); }); } DataStore.clearAll(); clearAllDropZonesState(); e.preventDefault(); }; /** * Clear the data store and remove the drag indicators from the UI * when the drag event has finished. */ var dragendHandler = function() { DataStore.clearAll(); clearAllDropZonesState(); }; /** * Re-render the drop zones in the new month to highlight * which areas are or aren't acceptable to drop the calendar * event. */ var calendarMonthChangedHandler = function() { updateAllDropZonesState(); }; return { /** * Initialise the event handlers for the drag events. */ init: function() { if (!registered) { // These handlers are only added the first time the module // is loaded because we don't want to have a new listener // added each time the "init" function is called otherwise we'll // end up with lots of stale handlers. document.addEventListener('dragstart', dragstartHandler, false); document.addEventListener('dragover', dragoverHandler, false); document.addEventListener('dragleave', dragleaveHandler, false); document.addEventListener('drop', dropHandler, false); document.addEventListener('dragend', dragendHandler, false); $('body').on(CalendarEvents.monthChanged, calendarMonthChangedHandler); registered = true; } }, }; }); PK*\d-uamd/src/events.jsnu[// 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 . /** * Contain the events the calendar component can fire. * * @module core_calendar/events * @copyright 2017 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ export default { created: 'calendar-events:created', deleted: 'calendar-events:deleted', deleteAll: 'calendar-events:delete_all', updated: 'calendar-events:updated', 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', courseChanged: 'calendar-events:course_changed', viewUpdated: 'calendar-events:view_updated', }; PK*\IcT amd/src/modal_delete.jsnu[// 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 . /** * Contain the logic for the delete modal. * * @module core_calendar/modal_delete * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import * as CustomEvents from 'core/custom_interaction_events'; import Modal from 'core/modal'; import ModalEvents from 'core/modal_events'; import CalendarEvents from './events'; const SELECTORS = { DELETE_ONE_BUTTON: '[data-action="deleteone"]', DELETE_ALL_BUTTON: '[data-action="deleteall"]', CANCEL_BUTTON: '[data-action="cancel"]', }; /** * Constructor for the Modal. * * @class * @param {object} root The root jQuery element for the modal */ export default class ModalDelete extends Modal { static TYPE = 'core_calendar-modal_delete'; static TEMPLATE = 'calendar/event_delete_modal'; constructor(root) { super(root); this.setRemoveOnClose(true); } /** * Set up all of the event handling for the modal. * * @method registerEventListeners */ registerEventListeners() { // Apply parent event listeners. super.registerEventListeners(this); this.getModal().on(CustomEvents.events.activate, SELECTORS.DELETE_ONE_BUTTON, (e, data) => { const saveEvent = $.Event(ModalEvents.save); this.getRoot().trigger(saveEvent, this); if (!saveEvent.isDefaultPrevented()) { this.hide(); data.originalEvent.preventDefault(); } }); this.getModal().on(CustomEvents.events.activate, SELECTORS.DELETE_ALL_BUTTON, (e, data) => { const saveEvent = $.Event(CalendarEvents.deleteAll); this.getRoot().trigger(saveEvent, this); if (!saveEvent.isDefaultPrevented()) { this.hide(); data.originalEvent.preventDefault(); } }); this.getModal().on(CustomEvents.events.activate, SELECTORS.CANCEL_BUTTON, (e, data) => { const cancelEvent = $.Event(ModalEvents.cancel); this.getRoot().trigger(cancelEvent, this); if (!cancelEvent.isDefaultPrevented()) { this.hide(); data.originalEvent.preventDefault(); } }); } } ModalDelete.registerModalType(); PK*\P߸11amd/src/calendar.jsnu[// 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 . /** * This module is the highest level module for the calendar. It is * responsible for initialising all of the components required for * the calendar to run. It also coordinates the interaction between * components by listening for and responding to different events * triggered within the calendar UI. * * @module core_calendar/calendar * @copyright 2017 Simey Lameze * @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', 'core_calendar/view_manager', 'core_calendar/crud', 'core_calendar/selectors', 'core/url', 'core/str', ], function( $, Templates, Notification, CalendarRepository, CalendarEvents, CalendarViewManager, CalendarCrud, CalendarSelectors, Url, Str, ) { var SELECTORS = { ROOT: "[data-region='calendar']", DAY: "[data-region='day']", NEW_EVENT_BUTTON: "[data-action='new-event-button']", DAY_CONTENT: "[data-region='day-content']", LOADING_ICON: '.loading-icon', VIEW_DAY_LINK: "[data-action='view-day-link']", CALENDAR_MONTH_WRAPPER: ".calendarwrapper", TODAY: '.today', DAY_NUMBER_CIRCLE: '.day-number-circle', DAY_NUMBER: '.day-number', SCREEN_READER_ANNOUNCEMENTS: '.calendar-announcements', CURRENT_MONTH: '.calendar-controls .current' }; /** * Handler for the drag and drop move event. Provides a loading indicator * while the request is sent to the server to update the event start date. * * Triggers a eventMoved calendar javascript event if the event was successfully * updated. * * @param {event} e The calendar move event * @param {int} eventId The event id being moved * @param {object|null} originElement The jQuery element for where the event is moving from * @param {object} destinationElement The jQuery element for where the event is moving to */ var handleMoveEvent = function(e, eventId, originElement, destinationElement) { var originTimestamp = null; var destinationTimestamp = destinationElement.attr('data-day-timestamp'); if (originElement) { originTimestamp = originElement.attr('data-day-timestamp'); } // If the event has actually changed day. if (!originElement || originTimestamp != destinationTimestamp) { Templates.render('core/loading', {}) .then(function(html, js) { // First we show some loading icons in each of the days being affected. destinationElement.find(SELECTORS.DAY_CONTENT).addClass('hidden'); Templates.appendNodeContents(destinationElement, html, js); if (originElement) { originElement.find(SELECTORS.DAY_CONTENT).addClass('hidden'); Templates.appendNodeContents(originElement, html, js); } return; }) .then(function() { // Send a request to the server to make the change. return CalendarRepository.updateEventStartDay(eventId, destinationTimestamp); }) .then(function() { // If the update was successful then broadcast an event letting the calendar // know that an event has been moved. $('body').trigger(CalendarEvents.eventMoved, [eventId, originElement, destinationElement]); return; }) .always(function() { // Always remove the loading icons regardless of whether the update // request was successful or not. var destinationLoadingElement = destinationElement.find(SELECTORS.LOADING_ICON); destinationElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden'); Templates.replaceNode(destinationLoadingElement, '', ''); if (originElement) { var originLoadingElement = originElement.find(SELECTORS.LOADING_ICON); originElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden'); Templates.replaceNode(originLoadingElement, '', ''); } return; }) .catch(Notification.exception); } }; /** * Listen to and handle any calendar events fired by the calendar UI. * * @method registerCalendarEventListeners * @param {object} root The calendar root element * @param {object} eventFormModalPromise A promise reolved with the event form modal */ var registerCalendarEventListeners = function(root, eventFormModalPromise) { var body = $('body'); body.on(CalendarEvents.created, function() { CalendarViewManager.reloadCurrentMonth(root); }); body.on(CalendarEvents.deleted, function() { CalendarViewManager.reloadCurrentMonth(root); }); body.on(CalendarEvents.updated, function() { CalendarViewManager.reloadCurrentMonth(root); }); body.on(CalendarEvents.editActionEvent, function(e, url) { // Action events needs to be edit directly on the course module. window.location.assign(url); }); // Handle the event fired by the drag and drop code. body.on(CalendarEvents.moveEvent, handleMoveEvent); // When an event is successfully moved we should updated the UI. body.on(CalendarEvents.eventMoved, function() { CalendarViewManager.reloadCurrentMonth(root); }); // Announce the newly loaded month to screen readers. body.on(CalendarEvents.monthChanged, root, async function() { const monthName = body.find(SELECTORS.CURRENT_MONTH).text(); const monthAnnoucement = await Str.get_string('newmonthannouncement', 'calendar', monthName); body.find(SELECTORS.SCREEN_READER_ANNOUNCEMENTS).html(monthAnnoucement); }); CalendarCrud.registerEditListeners(root, eventFormModalPromise); }; /** * Register event listeners for the module. * * @param {object} root The calendar root element * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block. */ var registerEventListeners = function(root, isCalendarBlock) { const viewingFullCalendar = document.getElementById(CalendarSelectors.fullCalendarView); // Listen the click on the day link to render the day view. root.on('click', SELECTORS.VIEW_DAY_LINK, function(e) { var dayLink = $(e.target).closest(SELECTORS.VIEW_DAY_LINK); var year = dayLink.data('year'), month = dayLink.data('month'), day = dayLink.data('day'), courseId = dayLink.data('courseid'), categoryId = dayLink.data('categoryid'); const urlParams = { view: 'day', time: dayLink.data('timestamp'), course: courseId, }; if (viewingFullCalendar) { // Construct the URL parameter string from the urlParams object. const urlParamString = Object.entries(urlParams) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); CalendarViewManager.refreshDayContent(root, year, month, day, courseId, categoryId, root, 'core_calendar/calendar_day', isCalendarBlock).then(function() { e.preventDefault(); // Update the URL if it's not calendar block. if (!isCalendarBlock) { CalendarViewManager.updateUrl('?' + urlParamString); } return; }).catch(Notification.exception); } else { window.location.assign(Url.relativeUrl('calendar/view.php', urlParams)); } }); root.on('change', CalendarSelectors.elements.courseSelector, function() { var selectElement = $(this); var courseId = selectElement.val(); const courseName = $("option:selected", selectElement).text(); CalendarViewManager.reloadCurrentMonth(root, courseId, null) .then(function() { // We need to get the selector again because the content has changed. return root.find(CalendarSelectors.elements.courseSelector).val(courseId); }) .then(function() { CalendarViewManager.updateUrl('?view=month&course=' + courseId); CalendarViewManager.handleCourseChange(Number(courseId), courseName); return; }) .catch(Notification.exception); }); var eventFormPromise = CalendarCrud.registerEventFormModal(root), contextId = $(SELECTORS.CALENDAR_MONTH_WRAPPER).data('context-id'); registerCalendarEventListeners(root, eventFormPromise); if (contextId) { // Bind click events to calendar days. root.on('click', SELECTORS.DAY, function(e) { var target = $(e.target); const displayingSmallBlockCalendar = root.parents('aside').data('blockregion') === 'side-pre'; if (!viewingFullCalendar && displayingSmallBlockCalendar) { const dateContainer = target.closest(SELECTORS.DAY); const wrapper = target.closest(CalendarSelectors.wrapper); const courseId = wrapper.data('courseid'); const params = { view: 'day', time: dateContainer.data('day-timestamp'), course: courseId, }; window.location.assign(Url.relativeUrl('calendar/view.php', params)); } else { const hasViewDayLink = target.closest(SELECTORS.VIEW_DAY_LINK).length; const shouldShowNewEventModal = !hasViewDayLink; if (shouldShowNewEventModal) { 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; }).catch(Notification.exception); } } e.preventDefault(); }); } }; return { /** * Initializes the calendar view manager and registers event listeners. * * @param {HTMLElement} root - The root element where the calendar view manager and event listeners will be attached. * @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block. */ init: function(root, isCalendarBlock = false) { root = $(root); CalendarViewManager.init(root, 'month', isCalendarBlock); registerEventListeners(root, isCalendarBlock); } }; }); PK*\;{amd/src/calendar_filter.jsnu[// 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 . /** * This module is responsible for the calendar filter. * * @module core_calendar/calendar_filter * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core_calendar/selectors', 'core_calendar/events', 'core/str', 'core/templates', ], function( $, CalendarSelectors, CalendarEvents, Str, Templates ) { var registerEventListeners = function(root) { root.on('click', CalendarSelectors.eventFilterItem, function(e) { var target = $(e.currentTarget); toggleFilter(target); e.preventDefault(); }); $('body').on(CalendarEvents.viewUpdated, function() { var filters = root.find(CalendarSelectors.eventFilterItem); filters.each(function(i, filter) { filter = $(filter); if (filter.data('eventtype-hidden')) { var data = getFilterData(filter); fireFilterChangedEvent(data); } }); }); }; var toggleFilter = function(target) { var data = getFilterData(target); // Toggle the hidden. We need to render the template before we change the value. data.hidden = !data.hidden; M.util.js_pending("core_calendar/calendar_filter:toggleFilter"); return Str.get_string('eventtype' + data.eventtype, 'calendar') .then(function(nameStr) { data.name = nameStr; data.icon = true; data.key = 'i/' + data.eventtype + 'event'; data.component = 'core'; return data; }) .then(function(context) { return Templates.render('core_calendar/event_filter_key', context); }) .then(function(html, js) { return Templates.replaceNode(target, html, js); }) .then(function() { fireFilterChangedEvent(data); M.util.js_complete("core_calendar/calendar_filter:toggleFilter"); return; }); }; /** * Fire the filterChanged event for the specified data. * * @param {object} data The data to include */ var fireFilterChangedEvent = function(data) { M.util.js_pending("month-mini-filterChanged"); $('body').trigger(CalendarEvents.filterChanged, { type: data.eventtype, hidden: data.hidden, }); M.util.js_complete("month-mini-filterChanged"); }; /** * Get the filter data for the specified target. * * @param {jQuery} target The target node * @return {Object} */ var getFilterData = function(target) { return { eventtype: target.data('eventtype'), hidden: target.data('eventtype-hidden'), }; }; return { init: function(root) { root = $(root); registerEventListeners(root); } }; }); PK*\Φamd/src/calendar_mini.jsnu[// 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 . /** * This module is the highest level module for the calendar. It is * responsible for initialising all of the components required for * the calendar to run. It also coordinates the interaction between * components by listening for and responding to different events * triggered within the calendar UI. * * @module core_calendar/calendar_mini * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core_calendar/selectors', 'core_calendar/events', 'core_calendar/view_manager', ], function( $, CalendarSelectors, CalendarEvents, CalendarViewManager ) { /** * Listen to and handle any calendar events fired by the calendar UI. * * @method registerCalendarEventListeners * @param {object} root The calendar root element */ var registerCalendarEventListeners = function(root) { var body = $('body'); var namespace = '.' + root.attr('id'); body.on(CalendarEvents.created + namespace, root, reloadMonth); body.on(CalendarEvents.deleted + namespace, root, reloadMonth); body.on(CalendarEvents.updated + namespace, root, reloadMonth); body.on(CalendarEvents.eventMoved + namespace, root, reloadMonth); }; /** * Reload the month view in this month. * * @param {EventFacade} e */ var reloadMonth = function(e) { var root = e.data; var body = $('body'); var namespace = '.' + root.attr('id'); if (root.is(':visible')) { CalendarViewManager.reloadCurrentMonth(root); } else { // The root has been removed. // Remove all events in the namespace. body.off(CalendarEvents.created + namespace); body.off(CalendarEvents.deleted + namespace); body.off(CalendarEvents.updated + namespace); body.off(CalendarEvents.eventMoved + namespace); } }; var registerEventListeners = function(root) { $('body').on(CalendarEvents.filterChanged, function(e, data) { var daysWithEvent = root.find(CalendarSelectors.eventType[data.type]); daysWithEvent.toggleClass('calendar_event_' + data.type, !data.hidden); }); var namespace = '.' + root.attr('id'); $('body').on('change' + namespace, CalendarSelectors.elements.courseSelector, function() { if (root.is(':visible')) { var selectElement = $(this); var courseId = selectElement.val(); var categoryId = null; CalendarViewManager.reloadCurrentMonth(root, courseId, categoryId); } else { $('body').off('change' + namespace); } }); }; return { init: function(root, loadOnInit) { root = $(root); CalendarViewManager.init(root); registerEventListeners(root); registerCalendarEventListeners(root); if (loadOnInit) { // The calendar hasn't yet loaded it's events so we // should load them as soon as we've initialised. CalendarViewManager.reloadCurrentMonth(root); } } }; }); PK*\vr amd/src/event_form.jsnu[// 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 . /** * A javascript module to enhance the event form. * * @module core_calendar/event_form * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core_calendar/repository', 'core/notification'], function($, CalendarRepository, Notification) { var SELECTORS = { EVENT_GROUP_COURSE_ID: '[name="groupcourseid"]', EVENT_GROUP_ID: '[name="groupid"]', SELECT_OPTION: 'option' }; /** * Listen for when the user changes the group course when configuring * a group event and filter the options in the group select to only * show the groups available within the course the user has selected. * * @method addCourseGroupSelectListeners * @param {object} formElement The root form element */ var addCourseGroupSelectListeners = function(formElement) { var courseGroupSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID); var loadGroupSelectOptions = function(groups) { var groupSelect = formElement.find(SELECTORS.EVENT_GROUP_ID), groupSelectOptions = groupSelect.find(SELECTORS.SELECT_OPTION), courseGroups = $(groups); // Let's clear all options first. groupSelectOptions.remove(); groupSelect.prop("disabled", false); courseGroups.each(function(id, group) { $(groupSelect).append($("").attr("value", group.id).text(group.name)); }); }; // If the user choose a course in the selector do a WS request to get groups. courseGroupSelect.on('change', function() { var courseId = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID).val(); if (isNaN(courseId) || courseId <= 0) { loadGroupSelectOptions([]); } else { CalendarRepository.getCourseGroupsData(courseId) .then(function(groups) { return loadGroupSelectOptions(groups); }) .catch(Notification.exception); } }); }; /** * Initialise all of the form enhancements. * * @method init * @param {string} formId The value of the form's id attribute */ var init = function(formId) { var formElement = $('#' + formId); addCourseGroupSelectListeners(formElement); }; return { init: init, }; }); PK*\ ۵amd/src/summary_modal.jsnu[// 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 . /** * A javascript module to handle summary modal. * * @module core_calendar/summary_modal * @copyright 2017 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import * as CustomEvents from 'core/custom_interaction_events'; import Modal from 'core/modal'; import CalendarEvents from './events'; import * as CalendarCrud from 'core_calendar/crud'; import * as ModalEvents from 'core/modal_events'; const SELECTORS = { ROOT: "[data-region='summary-modal-container']", EDIT_BUTTON: '[data-action="edit"]', DELETE_BUTTON: '[data-action="delete"]', }; export default class ModalEventSummary extends Modal { static TEMPLATE = 'core_calendar/event_summary_modal'; static TYPE = 'core_calendar-event_summary'; /** * Get the edit button element from the footer. The button is cached * as it's not expected to change. * * @method getEditButton * @return {object} button element */ getEditButton() { if (typeof this.editButton == 'undefined') { this.editButton = this.getFooter().find(SELECTORS.EDIT_BUTTON); } return this.editButton; } /** * Get the delete button element from the footer. The button is cached * as it's not expected to change. * * @method getDeleteButton * @return {object} button element */ getDeleteButton() { if (typeof this.deleteButton == 'undefined') { this.deleteButton = this.getFooter().find(SELECTORS.DELETE_BUTTON); } return this.deleteButton; } /** * Get the id 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 getEventId * @return {int} */ getEventId() { 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} */ getEventTitle() { 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 * depending on which event is being displayed. * * @method getEventCount * @return {int} */ getEventCount() { return this.getBody().find(SELECTORS.ROOT).attr('data-event-count'); } /** * Get the url for the event being shown in this modal. * * @method getEventUrl * @return {String} */ getEditUrl() { return this.getBody().find(SELECTORS.ROOT).attr('data-edit-url'); } /** * Is this an action event. * * @method getEventUrl * @return {String} */ isActionEvent() { return (this.getBody().find(SELECTORS.ROOT).attr('data-action-event') == 'true'); } /** * Set up all of the event handling for the modal. * * @method registerEventListeners */ registerEventListeners() { // Apply parent event listeners. super.registerEventListeners(this); // 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. M.util.js_pending('core_calendar/summary_modal:registerEventListeners:bodyRendered'); this.getRoot().on(ModalEvents.bodyRendered, function() { this.getModal().data({ eventTitle: this.getEventTitle(), eventId: this.getEventId(), eventCount: this.getEventCount(), }) .attr('data-type', 'event'); CalendarCrud.registerRemove(this.getModal()); M.util.js_complete('core_calendar/summary_modal:registerEventListeners:bodyRendered'); }.bind(this)); $('body').on(CalendarEvents.deleted, function() { // Close the dialogue on delete. this.hide(); }.bind(this)); CustomEvents.define(this.getEditButton(), [ CustomEvents.events.activate ]); 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()]); } else { // When the edit button is clicked we fire an event for the calendar UI to handle. // We don't care how the UI chooses to handle it. $('body').trigger(CalendarEvents.editEvent, [this.getEventId()]); } // There is nothing else for us to do so let's hide. this.hide(); // We've handled this event so no need to propagate it. e.preventDefault(); e.stopPropagation(); data.originalEvent.preventDefault(); data.originalEvent.stopPropagation(); }.bind(this)); } } ModalEventSummary.registerModalType(); PK*\ɾ$%%%amd/src/month_navigation_drag_drop.jsnu[// 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 . /** * A javascript module to handle calendar drag and drop in the calendar * month view navigation. * * This code is run each time the calendar month view is re-rendered. We * only register the event handlers once per page load so that the in place * DOM updates that happen on month change don't continue to register handlers. * * @module core_calendar/month_navigation_drag_drop * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core_calendar/drag_drop_data_store', ], function( $, DataStore ) { var SELECTORS = { DRAGGABLE: '[draggable="true"][data-region="event-item"]', DROP_ZONE: '[data-drop-zone="nav-link"]', }; var HOVER_CLASS = 'bg-primary text-white'; var TARGET_CLASS = 'drop-target'; var HOVER_TIME = 1000; // 1 second hover to change month. // We store some static variables at the module level because this // module is called each time the calendar month view is reloaded but // we want some actions to only occur ones. /* @var {bool} registered If the event listeners have been added */ var registered = false; /* @var {int} hoverTimer The timeout id of any timeout waiting for hover */ var hoverTimer = null; /* @var {object} root The root nav element we're operating on */ var root = null; /** * Add or remove the appropriate styling to indicate whether * the drop target is being hovered over. * * @param {object} target The target drop zone element * @param {bool} hovered If the element is hovered over ot not */ var updateHoverState = function(target, hovered) { if (hovered) { target.addClass(HOVER_CLASS); } else { target.removeClass(HOVER_CLASS); } }; /** * Add some styling to the UI to indicate that the nav links * are an acceptable drop target. */ var addDropZoneIndicator = function() { root.find(SELECTORS.DROP_ZONE).addClass(TARGET_CLASS); }; /** * Remove the styling from the nav links. */ var removeDropZoneIndicator = function() { root.find(SELECTORS.DROP_ZONE).removeClass(TARGET_CLASS); }; /** * Get the drop zone target from the event, if one is found. * * @param {event} e Javascript event * @return {object|null} */ var getTargetFromEvent = function(e) { var target = $(e.target).closest(SELECTORS.DROP_ZONE); return (target.length) ? target : null; }; /** * This will add a visual indicator to the calendar UI to * indicate which nav link is a valid drop zone. * * @param {Event} e */ var dragstartHandler = function(e) { // Make sure the drag event is for a calendar event. var eventElement = $(e.target).closest(SELECTORS.DRAGGABLE); if (eventElement.length) { addDropZoneIndicator(); } }; /** * Update the hover state of the target nav element when * the user is dragging an event over it. * * This will add a visual indicator to the calendar UI to * indicate which nav link is being hovered. * * @param {event} e The dragover event */ var dragoverHandler = function(e) { // Ignore dragging of non calendar events. if (!DataStore.hasEventId()) { return; } e.preventDefault(); var target = getTargetFromEvent(e); if (!target) { return; } // If we're not draggin a calendar event then // ignore it. if (!DataStore.hasEventId()) { return; } if (!hoverTimer) { hoverTimer = setTimeout(function() { target.click(); hoverTimer = null; }, HOVER_TIME); } updateHoverState(target, true); removeDropZoneIndicator(); }; /** * Update the hover state of the target nav element that was * previously dragged over but has is no longer a drag target. * * This will remove the visual indicator from the calendar UI * that was added by the dragoverHandler. * * @param {event} e The dragstart event */ var dragleaveHandler = function(e) { // Ignore dragging of non calendar events. if (!DataStore.hasEventId()) { return; } var target = getTargetFromEvent(e); if (!target) { return; } if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; } updateHoverState(target, false); addDropZoneIndicator(); e.preventDefault(); }; /** * Remove the visual indicator from the calendar UI that was * added by the dragoverHandler. * * @param {event} e The drop event */ var dropHandler = function(e) { // Ignore dragging of non calendar events. if (!DataStore.hasEventId()) { return; } removeDropZoneIndicator(); var target = getTargetFromEvent(e); if (!target) { return; } updateHoverState(target, false); e.preventDefault(); }; return { /** * Initialise the event handlers for the drag events. * * @param {object} rootElement The element containing calendar nav links */ init: function(rootElement) { // Only register the handlers once on the first load. if (!registered) { // These handlers are only added the first time the module // is loaded because we don't want to have a new listener // added each time the "init" function is called otherwise we'll // end up with lots of stale handlers. document.addEventListener('dragstart', dragstartHandler, false); document.addEventListener('dragover', dragoverHandler, false); document.addEventListener('dragleave', dragleaveHandler, false); document.addEventListener('drop', dropHandler, false); document.addEventListener('dragend', removeDropZoneIndicator, false); registered = true; } // Update the module variable to operate on the given // root element. root = $(rootElement); // If we're currently dragging then add the indicators. if (DataStore.hasEventId()) { addDropZoneIndicator(); } }, }; }); PK*\2YYamd/src/view_manager.jsnu[// 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 . /** * A javascript module to handler calendar view changes. * * @module core_calendar/view_manager * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import Templates from 'core/templates'; import Notification from 'core/notification'; import * as CalendarRepository from 'core_calendar/repository'; import CalendarEvents from 'core_calendar/events'; import * as CalendarSelectors from 'core_calendar/selectors'; import ModalEvents from 'core/modal_events'; import SummaryModal from 'core_calendar/summary_modal'; import CustomEvents from 'core/custom_interaction_events'; import {getString} from 'core/str'; import Pending from 'core/pending'; import {prefetchStrings} from 'core/prefetch'; import Url from 'core/url'; import Config from 'core/config'; /** * Limit number of events per day * */ const LIMIT_DAY_EVENTS = 5; /** * Hide day events if more than 5. * */ export const foldDayEvents = () => { const root = $(CalendarSelectors.elements.monthDetailed); const days = root.find(CalendarSelectors.day); if (days.length === 0) { return; } days.each(function() { const dayContainer = $(this); const eventsSelector = `${CalendarSelectors.elements.dateContent} ul li[data-event-eventtype]`; const filteredEventsSelector = `${CalendarSelectors.elements.dateContent} ul li[data-event-filtered="true"]`; const moreEventsSelector = `${CalendarSelectors.elements.dateContent} [data-action="view-more-events"]`; const events = dayContainer.find(eventsSelector); if (events.length === 0) { return; } const filteredEvents = dayContainer.find(filteredEventsSelector); const numberOfFiltered = filteredEvents.length; const numberOfEvents = events.length - numberOfFiltered; let count = 1; events.each(function() { const event = $(this); const isNotFiltered = event.attr('data-event-filtered') !== 'true'; const offset = (numberOfEvents === LIMIT_DAY_EVENTS) ? 0 : 1; if (isNotFiltered) { if (count > LIMIT_DAY_EVENTS - offset) { event.attr('data-event-folded', 'true'); event.hide(); } else { event.attr('data-event-folded', 'false'); event.show(); count++; } } else { // It's being filtered out. event.attr('data-event-folded', 'false'); } }); const moreEventsLink = dayContainer.find(moreEventsSelector); if (numberOfEvents > LIMIT_DAY_EVENTS) { const numberOfHiddenEvents = numberOfEvents - LIMIT_DAY_EVENTS + 1; moreEventsLink.show(); getString('moreevents', 'calendar', numberOfHiddenEvents).then(str => { const link = moreEventsLink.find('strong a'); moreEventsLink.attr('data-event-folded', 'false'); link.text(str); return str; }).catch(Notification.exception); } else { moreEventsLink.hide(); } }); }; /** * Register and handle month calendar events. * * @param {string} pendingId pending id. */ export const registerEventListenersForMonthDetailed = (pendingId) => { const events = `${CalendarEvents.viewUpdated}`; $('body').on(events, function(e) { foldDayEvents(e); }); foldDayEvents(); $('body').on(CalendarEvents.filterChanged, function(e, data) { const root = $(CalendarSelectors.elements.monthDetailed); const pending = new Pending(pendingId); const target = root.find(CalendarSelectors.eventType[data.type]); const transitionPromise = $.Deferred(); if (data.hidden) { transitionPromise.then(function() { target.attr('data-event-filtered', 'true'); return target.hide().promise(); }).fail(); } else { transitionPromise.then(function() { target.attr('data-event-filtered', 'false'); return target.show().promise(); }).fail(); } transitionPromise.then(function() { foldDayEvents(); return; }) .always(pending.resolve) .fail(); transitionPromise.resolve(); }); }; /** * Register event listeners for the module. * * @param {object} root The root element. * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block. */ const registerEventListeners = (root, isCalendarBlock) => { root = $(root); // Bind click events to event links. root.on('click', CalendarSelectors.links.eventLink, (e) => { const target = e.target; let eventLink = null; let eventId = null; const pendingPromise = new Pending('core_calendar/view_manager:eventLink:click'); if (target.matches(CalendarSelectors.actions.viewEvent)) { eventLink = target; } else { eventLink = target.closest(CalendarSelectors.actions.viewEvent); } if (eventLink) { eventId = eventLink.dataset.eventId; } else { eventId = target.querySelector(CalendarSelectors.actions.viewEvent).dataset.eventId; } if (eventId) { // A link was found. Show the modal. e.preventDefault(); // We've handled the event so stop it from bubbling // and causing the day click handler to fire. e.stopPropagation(); renderEventSummaryModal(eventId) .then(pendingPromise.resolve) .catch(); } else { pendingPromise.resolve(); } }); root.on('click', CalendarSelectors.links.navLink, (e) => { const wrapper = root.find(CalendarSelectors.wrapper); const view = wrapper.data('view'); const courseId = wrapper.data('courseid'); const categoryId = wrapper.data('categoryid'); const link = e.currentTarget; if (view === 'month' || view === 'monthblock') { changeMonth(root, link.href, link.dataset.year, link.dataset.month, courseId, categoryId, link.dataset.day, isCalendarBlock); e.preventDefault(); } else if (view === 'day') { changeDay(root, link.href, link.dataset.year, link.dataset.month, link.dataset.day, courseId, categoryId, isCalendarBlock); e.preventDefault(); } }); const viewSelector = root.find(CalendarSelectors.viewSelector); CustomEvents.define(viewSelector, [CustomEvents.events.activate]); viewSelector.on( CustomEvents.events.activate, (e) => { e.preventDefault(); const option = e.target; if (option.classList.contains('active')) { return; } const view = option.dataset.view, year = option.dataset.year, month = option.dataset.month, day = option.dataset.day, courseId = option.dataset.courseid, categoryId = option.dataset.categoryid; if (view == 'month') { refreshMonthContent(root, year, month, courseId, categoryId, root, 'core_calendar/calendar_month', day) .then(() => { updateUrl('?view=month&course=' + courseId); return; }).fail(Notification.exception); } else if (view == 'day') { refreshDayContent(root, year, month, day, courseId, categoryId, root, 'core_calendar/calendar_day') .then(() => { updateUrl('?view=day&course=' + courseId); return; }).fail(Notification.exception); } else if (view == 'upcoming') { reloadCurrentUpcoming(root, courseId, categoryId, root, 'core_calendar/calendar_upcoming') .then(() => { updateUrl('?view=upcoming&course=' + courseId); return; }).fail(Notification.exception); } } ); }; /** * Refresh the month content. * * @param {object} root The root element. * @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. * @param {string} template The template to be rendered. * @param {number} day Day (optional) * @return {promise} */ export const refreshMonthContent = (root, year, month, courseId, categoryId, target = null, template = '', day = 1) => { startLoading(root); target = target || root.find(CalendarSelectors.wrapper); template = template || root.attr('data-template'); M.util.js_pending([root.get('id'), year, month, courseId].join('-')); const includenavigation = root.data('includenavigation'); const mini = root.data('mini'); const viewMode = target.data('view'); return CalendarRepository.getCalendarMonthData(year, month, courseId, categoryId, includenavigation, mini, day, viewMode) .then(context => { return Templates.render(template, context); }) .then((html, js) => { return Templates.replaceNode(target, html, js); }) .then(() => { document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated)); return; }) .always(() => { M.util.js_complete([root.get('id'), year, month, courseId].join('-')); return stopLoading(root); }) .fail(Notification.exception); }; /** * Handle changes to the current calendar view. * * @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 * @param {number} day Day (optional) * @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block. * @return {promise} */ export const changeMonth = (root, url, year, month, courseId, categoryId, day = 1, isCalendarBlock = false) => { return refreshMonthContent(root, year, month, courseId, categoryId, null, '', day) .then((...args) => { if (url.length && url !== '#' && !isCalendarBlock) { updateUrl(url); } return args; }) .then((...args) => { $('body').trigger(CalendarEvents.monthChanged, [year, month, courseId, categoryId, day, isCalendarBlock]); return args; }); }; /** * Reload the current month 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} */ export const reloadCurrentMonth = (root, courseId = 0, categoryId = 0) => { const year = root.find(CalendarSelectors.wrapper).data('year'); const month = root.find(CalendarSelectors.wrapper).data('month'); const day = root.find(CalendarSelectors.wrapper).data('day'); courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid'); categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid'); return refreshMonthContent(root, year, month, courseId, categoryId, null, '', day). then((...args) => { $('body').trigger(CalendarEvents.courseChanged, [year, month, courseId, categoryId]); return args; }); }; /** * 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. * @param {string} template The template to be rendered. * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block. * * @return {promise} */ export const refreshDayContent = (root, year, month, day, courseId, categoryId, target = null, template = '', isCalendarBlock = false) => { startLoading(root); if (!target || target.length == 0){ target = root.find(CalendarSelectors.wrapper); } template = template || root.attr('data-template'); M.util.js_pending([root.get('id'), year, month, day, courseId, categoryId].join('-')); const includenavigation = root.data('includenavigation'); return CalendarRepository.getCalendarDayData(year, month, day, courseId, categoryId, includenavigation) .then((context) => { context.viewingday = true; context.showviewselector = true; context.iscalendarblock = isCalendarBlock; return Templates.render(template, context); }) .then((html, js) => { return Templates.replaceNode(target, html, js); }) .then(() => { document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated)); return; }) .always(() => { 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} */ export const reloadCurrentDay = (root, courseId = 0, categoryId = 0) => { const wrapper = root.find(CalendarSelectors.wrapper); const year = wrapper.data('year'); const month = wrapper.data('month'); const day = wrapper.data('day'); courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid'); categoryId = 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 * @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block. * @return {promise} */ export const changeDay = (root, url, year, month, day, courseId, categoryId, isCalendarBlock = false) => { return refreshDayContent(root, year, month, day, courseId, categoryId, null, '', isCalendarBlock) .then((...args) => { if (url.length && url !== '#' && !isCalendarBlock) { updateUrl(url); } return args; }) .then((...args) => { $('body').trigger(CalendarEvents.dayChanged, [year, month, courseId, categoryId, isCalendarBlock]); return args; }); }; /** * Update calendar URL. * * @param {String} url The calendar url to be updated. */ export const updateUrl = (url) => { const viewingFullCalendar = document.getElementById(CalendarSelectors.fullCalendarView); // We want to update the url only if the user is viewing the full calendar. if (viewingFullCalendar) { window.history.pushState({}, '', url); } }; /** * Set the element state to loading. * * @param {object} root The container element * @method startLoading */ const startLoading = (root) => { const loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon); loadingIconContainer.removeClass('hidden'); }; /** * Remove the loading state from the element. * * @param {object} root The container element * @method stopLoading */ const stopLoading = (root) => { const loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon); loadingIconContainer.addClass('hidden'); }; /** * Reload the current month 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 * @param {object} target The element being replaced. If not specified, the calendarwrapper is used. * @param {string} template The template to be rendered. * @return {promise} */ export const reloadCurrentUpcoming = (root, courseId = 0, categoryId = 0, target = null, template = '') => { startLoading(root); target = target || root.find(CalendarSelectors.wrapper); template = template || root.attr('data-template'); courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid'); categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid'); return CalendarRepository.getCalendarUpcomingData(courseId, categoryId) .then((context) => { context.viewingupcoming = true; context.showviewselector = true; return Templates.render(template, context); }) .then((html, js) => { return Templates.replaceNode(target, html, js); }) .then(() => { document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated)); return; }) .always(function() { return stopLoading(root); }) .fail(Notification.exception); }; /** * Get the CSS class to apply for the given event type. * * @param {string} eventType The calendar event type * @return {string} */ const getEventTypeClassFromType = (eventType) => { return 'calendar_event_' + eventType; }; /** * Render the event summary modal. * * @param {Number} eventId The calendar event id. * @returns {Promise} */ const renderEventSummaryModal = (eventId) => { const pendingPromise = new Pending('core_calendar/view_manager:renderEventSummaryModal'); // Calendar repository promise. return CalendarRepository.getEventById(eventId) .then((getEventResponse) => { if (!getEventResponse.event) { throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId); } return getEventResponse.event; }) .then(eventData => { // Build the modal parameters from the event data. const modalParams = { title: eventData.name, body: Templates.render('core_calendar/event_summary_body', eventData), templateContext: { canedit: eventData.canedit, candelete: eventData.candelete, headerclasses: getEventTypeClassFromType(eventData.normalisedeventtype), isactionevent: eventData.isactionevent, url: eventData.url, action: eventData.action } }; // Create the modal. return SummaryModal.create(modalParams); }) .then(modal => { // Handle hidden event. modal.getRoot().on(ModalEvents.hidden, function() { // Destroy when hidden. modal.destroy(); }); // Finally, render the modal! modal.show(); return modal; }) .then(modal => { pendingPromise.resolve(); return modal; }) .catch(Notification.exception); }; /** * Initializes the calendar component by prefetching strings, folding day events, * and registering event listeners. * * @param {HTMLElement} root - The root element where the calendar view manager and event listeners will be attached. * @param {string} view - A flag indicating whether this is a calendar block. * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block. */ export const init = (root, view, isCalendarBlock) => { prefetchStrings('calendar', ['moreevents']); foldDayEvents(); registerEventListeners(root, isCalendarBlock); const calendarTable = root.find(CalendarSelectors.elements.monthDetailed); if (calendarTable.length) { const pendingId = `month-detailed-${calendarTable.id}-filterChanged`; registerEventListenersForMonthDetailed(calendarTable, pendingId); } }; /** * Handles the change of course and updates the relevant elements on the page. * * @param {integer} courseId - The ID of the new course. * @param {string} courseName - The name of the new course. * @returns {Promise} - A promise that resolves after the updates are applied. */ export const handleCourseChange = async(courseId, courseName) => { // Select the

    element inside the data-region="view-selector". const ulElement = document.querySelector(CalendarSelectors.viewSelector + ' ul'); // Select all
  • elements within the
      . const liElements = ulElement.querySelectorAll('li a'); // Loop through the selected elements and update the courseid. liElements.forEach(element => { element.setAttribute('data-courseid', courseId); }); const calendar = await getString('calendar', 'calendar'); const pageHeaderHeadingsElement = document.querySelector(CalendarSelectors.pageHeaderHeadings); const courseUrl = Url.relativeUrl('/course/view.php', {id: courseId}); // Apply the page header text. if (courseId !== Config.siteId) { pageHeaderHeadingsElement.innerHTML = calendar + ': ' + courseName + ''; } else { pageHeaderHeadingsElement.innerHTML = calendar; } };PK*\X`namd/src/drag_drop_data_store.jsnu[// 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 . /** * A javascript module to store calendar drag and drop data. * * This module is unfortunately required because of the limitations * of the HTML5 drag and drop API and it's ability to provide data * between the different stages of the drag/drop lifecycle. * * @module core_calendar/drag_drop_data_store * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([], function() { /* @var {int|null} eventId The id of the event being dragged */ var eventId = null; /* @var {int|null} durationDays How many days the event spans */ var durationDays = null; /* @var {int|null} minTimestart The earliest valid timestart */ var minTimestart = null; /* @var {int|null} maxTimestart The latest valid tiemstart */ var maxTimestart = null; /* @var {string|null} minError Error message for min timestamp violation */ var minError = null; /* @var {string|null} maxError Error message for max timestamp violation */ var maxError = null; /** * Store the id of the event being dragged. * * @param {int} id The event id */ var setEventId = function(id) { eventId = id; }; /** * Get the stored event id. * * @return {int|null} */ var getEventId = function() { return eventId; }; /** * Check if the store has an event id. * * @return {bool} */ var hasEventId = function() { return eventId !== null; }; /** * Store the duration (in days) of the event being dragged. * * @param {int} days Number of days the event spans */ var setDurationDays = function(days) { durationDays = days; }; /** * Get the stored number of days. * * @return {int|null} */ var getDurationDays = function() { return durationDays; }; /** * Store the minimum timestart valid for an event being dragged. * * @param {int} timestamp The unix timstamp */ var setMinTimestart = function(timestamp) { minTimestart = timestamp; }; /** * Get the minimum valid timestart. * * @return {int|null} */ var getMinTimestart = function() { return minTimestart; }; /** * Check if a minimum timestamp is set. * * @return {bool} */ var hasMinTimestart = function() { return minTimestart !== null; }; /** * Store the maximum timestart valid for an event being dragged. * * @param {int} timestamp The unix timstamp */ var setMaxTimestart = function(timestamp) { maxTimestart = timestamp; }; /** * Get the maximum valid timestart. * * @return {int|null} */ var getMaxTimestart = function() { return maxTimestart; }; /** * Check if a maximum timestamp is set. * * @return {bool} */ var hasMaxTimestart = function() { return maxTimestart !== null; }; /** * Store the error string to display if trying to drag an event * earlier than the minimum allowed date. * * @param {string} message The error message */ var setMinError = function(message) { minError = message; }; /** * Get the error message for a minimum time start violation. * * @return {string|null} */ var getMinError = function() { return minError; }; /** * Store the error string to display if trying to drag an event * later than the maximum allowed date. * * @param {string} message The error message */ var setMaxError = function(message) { maxError = message; }; /** * Get the error message for a maximum time start violation. * * @return {string|null} */ var getMaxError = function() { return maxError; }; /** * Reset all of the stored values. */ var clearAll = function() { setEventId(null); setDurationDays(null); setMinTimestart(null); setMaxTimestart(null); setMinError(null); setMaxError(null); }; return { setEventId: setEventId, getEventId: getEventId, hasEventId: hasEventId, setDurationDays: setDurationDays, getDurationDays: getDurationDays, setMinTimestart: setMinTimestart, getMinTimestart: getMinTimestart, hasMinTimestart: hasMinTimestart, setMaxTimestart: setMaxTimestart, getMaxTimestart: getMaxTimestart, hasMaxTimestart: hasMaxTimestart, setMinError: setMinError, getMinError: getMinError, setMaxError: setMaxError, getMaxError: getMaxError, clearAll: clearAll }; }); PK*\sRvvamd/src/repository.jsnu[// 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 . /** * A javascript module to handle calendar ajax actions. * * @module core_calendar/repository * @copyright 2017 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Ajax from 'core/ajax'; /** * Delete a calendar event. * * @method deleteEvent * @param {number} eventId The event id. * @param {boolean} deleteSeries Whether to delete all events in the series * @return {promise} Resolved with requested calendar event */ export const deleteEvent = (eventId, deleteSeries = false) => { const request = { methodname: 'core_calendar_delete_calendar_events', args: { events: [{ eventid: eventId, repeat: deleteSeries, }] } }; return Ajax.call([request])[0]; }; /** * Get a calendar event by id. * * @method getEventById * @param {number} eventId The event id. * @return {promise} Resolved with requested calendar event */ export const getEventById = (eventId) => { const request = { methodname: 'core_calendar_get_calendar_event_by_id', args: { eventid: eventId } }; return Ajax.call([request])[0]; }; /** * Submit the form data for the event form. * * @method submitCreateUpdateForm * @param {string} formData The URL encoded values from the form * @return {promise} Resolved with the new or edited event */ export const submitCreateUpdateForm = (formData) => { const request = { methodname: 'core_calendar_submit_create_update_form', args: { formdata: formData } }; return Ajax.call([request])[0]; }; /** * Get calendar data for the month view. * * @method getCalendarMonthData * @param {number} year Year * @param {number} month Month * @param {number} courseId The course id. * @param {number} categoryId The category id. * @param {boolean} includeNavigation Whether to include navigation. * @param {boolean} mini Whether the month is in mini view. * @param {number} day Day (optional) * @param {string} view The calendar view mode. * @return {promise} Resolved with the month view data. */ export const getCalendarMonthData = (year, month, courseId, categoryId, includeNavigation, mini, day = 1, view = 'month') => { const request = { methodname: 'core_calendar_get_calendar_monthly_view', args: { year, month, courseid: courseId, categoryid: categoryId, includenavigation: includeNavigation, mini, day, view, } }; 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. */ export const getCalendarDayData = (year, month, day, courseId, categoryId) => { const request = { methodname: 'core_calendar_get_calendar_day_view', args: { year, month, 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 * date information is extracted, the time of the day is ignored. * * @param {int} eventId The id of the event to update * @param {int} dayTimestamp A timestamp for some time during the target day * @return {promise} */ export const updateEventStartDay = (eventId, dayTimestamp) => { const request = { methodname: 'core_calendar_update_event_start_day', args: { eventid: eventId, daytimestamp: dayTimestamp } }; return Ajax.call([request])[0]; }; /** * Get calendar upcoming data. * * @method getCalendarUpcomingData * @param {number} courseId The course id. * @param {number} categoryId The category id. * @return {promise} Resolved with the month view data. */ export const getCalendarUpcomingData = (courseId, categoryId) => { const request = { methodname: 'core_calendar_get_calendar_upcoming_view', args: { courseid: courseId, categoryid: categoryId, } }; return Ajax.call([request])[0]; }; /** * Get the groups by course id. * * @param {Number} courseId The course id to fetch the groups from. * @return {promise} Resolved with the course groups. */ export const getCourseGroupsData = (courseId) => { const request = { methodname: 'core_group_get_course_groups', args: { courseid: courseId } }; return Ajax.call([request])[0]; }; /** * Delete calendar subscription by id. * * @param {Number} subscriptionId The subscription id * @return {promise} */ export const deleteSubscription = (subscriptionId) => { const request = { methodname: 'core_calendar_delete_subscription', args: { subscriptionid: subscriptionId } }; return Ajax.call([request])[0]; }; PK*\]>ssamd/src/export.jsnu[// 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 . /** * A javascript module to enhance the calendar export form. * * @module core_calendar/export * @copyright 2021 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import 'core/copy_to_clipboard'; /** * Selectors for the calendar export page. * * @property {string} copyUrlId The element ID of the Copy URL button. */ const selectors = { copyUrlId: 'copyexporturl', }; /** * Initialises the calendar export JS module. * * @method init */ export const init = () => { // Enable the copy URL button and focus on it. const copyUrl = document.getElementById(selectors.copyUrlId); copyUrl.removeAttribute('disabled'); copyUrl.focus(); }; PK*\r5WWamd/src/calendar_view.jsnu[// 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 . /** * This module is responsible for handle calendar day and upcoming view. * * @module core_calendar/calendar_view * @copyright 2017 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/notification', 'core_calendar/selectors', 'core_calendar/events', 'core_calendar/view_manager', 'core_calendar/crud' ], function( $, Notification, CalendarSelectors, CalendarEvents, CalendarViewManager, CalendarCrud ) { var registerEventListeners = function(root, type) { var body = $('body'); 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(); const courseName = $("option:selected", selectElement).text(); CalendarViewManager[reloadFunction](root, courseId, null) .then(function() { // We need to get the selector again because the content has changed. return root.find(CalendarSelectors.courseSelector).val(courseId); }) .then(function() { CalendarViewManager.updateUrl('?view=' + type + '&course=' + courseId); CalendarViewManager.handleCourseChange(Number(courseId), courseName); return; }) .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'); } CalendarViewManager.foldDayEvents(root); }); var eventFormPromise = CalendarCrud.registerEventFormModal(root); CalendarCrud.registerEditListeners(root, eventFormPromise); }; return { init: function(root, type, isCalendarBlock = false) { root = $(root); CalendarViewManager.init(root, type, isCalendarBlock); registerEventListeners(root, type, isCalendarBlock); } }; }); PK*\QC  amd/src/selectors.jsnu[// 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 . /** * CSS selectors for the calendar. * * @module core_calendar/selectors * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([], function() { return { 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]", other: "[data-eventtype-other]", }, 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]", other: "[data-popover-eventtype-other]", }, calendarPeriods: { month: "[data-period='month']", }, courseSelector: 'select[name="course"]', viewSelector: 'div[data-region="view-selector"]', actions: { create: '[data-action="new-event-button"]', edit: '[data-action="edit"]', remove: '[data-action="delete"]', viewEvent: '[data-action="view-event"]', deleteSubscription: '[data-action="delete-subscription"]', }, elements: { courseSelector: 'select[name="course"]', dateContainer: '.clickable.hasevent', dateContent: '[data-region="day-content"]', monthDetailed: '.calendarmonth.calendartable', }, today: '.today', day: '[data-region="day"]', calendarMain: '[data-region="calendar"]', wrapper: '.calendarwrapper', eventItem: '[data-type="event"]', links: { navLink: '.calendarwrapper .arrow_link', eventLink: "[data-region='event-item']", miniDayLink: "[data-region='mini-day-link']", dayLink: "[data-action='view-day-link']", }, containers: { loadingIcon: '[data-region="overlay-icon-container"]', }, mainCalendar: '.maincalendar .heightcontainer', fullCalendarView: 'page-calendar-view', pageHeaderHeadings: '.page-header-headings h1', }; }); PK*\`11amd/src/manage_subscriptions.jsnu[// 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 . /** * A module to handle Delete/Update operations of the manage subscription page. * * @module core_calendar/manage_subscriptions * @copyright 2021 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 4.0 */ import * as CalendarSelectors from 'core_calendar/selectors'; import * as CalendarRepository from 'core_calendar/repository'; import ModalSaveCancel from 'core/modal_save_cancel'; import * as ModalEvents from 'core/modal_events'; import {exception as displayException, addNotification, fetchNotifications} from 'core/notification'; import Prefetch from 'core/prefetch'; import {getString} from 'core/str'; import {eventTypes} from 'core/local/inplace_editable/events'; /** * Get subscription id for given element. * * @param {HTMLElement} element update/delete link * @return {Number} */ const getSubscriptionId = element => { return parseInt(element.closest('tr').dataset.subid); }; /** * Get subscription name for given element. * * @param {HTMLElement} element update/delete link * @return {String} */ const getSubscriptionName = element => { return element.closest('tr').dataset.subname; }; /** * Get subscription table row for subscription id. * * @param {string} subscriptionId Subscription id * @return {Element} */ const getSubscriptionRow = subscriptionId => { return document.querySelector(`tr[data-subid="${subscriptionId}"]`); }; /** * Create modal. * * @param {HTMLElement} element * @param {string} messageCode Message code. * @return {promise} Promise for modal */ const createModal = (element, messageCode) => { const subscriptionName = getSubscriptionName(element); return ModalSaveCancel.create({ title: getString('confirmation', 'admin'), body: getString(messageCode, 'calendar', subscriptionName), buttons: { save: getString('yes') }, }).then(modal => { modal.getRoot().on(ModalEvents.hidden, () => { element.focus(); }); modal.show(); return modal; }); }; /** * Response handler for delete action. * * @param {HTMLElement} element * @param {Object} data * @return {Promise} */ const responseHandlerForDelete = async(element, data) => { const subscriptionName = getSubscriptionName(element); const message = data.status ? await getString('subscriptionremoved', 'calendar', subscriptionName) : data.warnings[0].message; const type = data.status ? 'info' : 'error'; return addNotification({message, type}); }; /** * Register events for update/delete links. */ const registerEventListeners = () => { document.addEventListener('click', e => { const deleteAction = e.target.closest(CalendarSelectors.actions.deleteSubscription); if (deleteAction) { e.preventDefault(); const modalPromise = createModal(deleteAction, 'confirmsubscriptiondelete'); modalPromise.then(modal => { modal.getRoot().on(ModalEvents.save, () => { const subscriptionId = getSubscriptionId(deleteAction); CalendarRepository.deleteSubscription(subscriptionId).then(data => { const response = responseHandlerForDelete(deleteAction, data); return response.then(() => { const subscriptionRow = getSubscriptionRow(subscriptionId); return subscriptionRow.remove(); }); }).catch(displayException); }); return modal; }).catch(displayException); } }); document.addEventListener(eventTypes.elementUpdated, e => { const inplaceEditable = e.target; if (inplaceEditable.getAttribute('data-component') == 'core_calendar') { fetchNotifications(); } }); }; /** * Initialises. */ export const init = () => { Prefetch.prefetchStrings('moodle', ['yes']); Prefetch.prefetchStrings('core_admin', ['confirmation']); Prefetch.prefetchStrings('core_calendar', ['confirmsubscriptiondelete', 'subscriptionremoved']); registerEventListeners(); }; PK*\́amd/src/popover.jsnu[// 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 . /** * Javascript popover for the `core_calendar` subsystem. * * @module core_calendar/popover * @copyright 2021 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 4.0 */ import Popover from 'theme_boost/bootstrap/popover'; import * as CalendarSelectors from 'core_calendar/selectors'; /** * Check if we are allowing to enable the popover or not. * @param {Element} dateContainer * @returns {boolean} */ const isPopoverAvailable = (dateContainer) => { return window.getComputedStyle(dateContainer.querySelector(CalendarSelectors.elements.dateContent)).display === 'none'; }; const isPopoverConfigured = new Map(); const showPopover = target => { const dateContainer = target.closest(CalendarSelectors.elements.dateContainer); if (!isPopoverConfigured.has(dateContainer)) { const config = { trigger: 'manual', placement: 'top', html: true, title: dateContainer.dataset.title, content: () => { const source = dateContainer.querySelector(CalendarSelectors.elements.dateContent); return source ? source.querySelector('.hidden').innerHTML : ''; }, 'animation': false, }; new Popover(target, config); isPopoverConfigured.set(dateContainer, true); } if (isPopoverAvailable(dateContainer)) { Popover.getInstance(target).show(); target.addEventListener('mouseleave', hidePopover); target.addEventListener('focusout', hidePopover); // Set up the hide function to the click event type. target.addEventListener('click', hidePopover); } }; const hidePopover = e => { const target = e.target; const dateContainer = e.target.closest(CalendarSelectors.elements.dateContainer); if (!dateContainer) { return; } if (isPopoverConfigured.has(dateContainer)) { const isTargetActive = target.contains(document.activeElement); const isTargetHover = target.matches(':hover'); // Checks if a target element is clicked or pressed. const isTargetClicked = document.activeElement.contains(target); let removeListener = true; if (!isTargetActive && !isTargetHover) { Popover.getOrCreateInstance(target).hide(); } else if (isTargetClicked) { Popover.getOrCreateInstance(document.activeElement).hide(); } else { removeListener = false; } if (removeListener) { target.removeEventListener('mouseleave', hidePopover); target.removeEventListener('focusout', hidePopover); target.removeEventListener('click', hidePopover); } } }; /** * Register events for date container. */ const registerEventListeners = () => { const showPopoverHandler = (e) => { const dayLink = e.target.closest(CalendarSelectors.links.dayLink); if (!dayLink) { return; } e.preventDefault(); showPopover(dayLink); }; document.addEventListener('mouseover', showPopoverHandler); document.addEventListener('focusin', showPopoverHandler); }; let listenersRegistered = false; if (!listenersRegistered) { registerEventListeners(); listenersRegistered = true; } PK*\$L, , amd/build/repository.min.jsnu[define("core_calendar/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj; /** * A javascript module to handle calendar ajax actions. * * @module core_calendar/repository * @copyright 2017 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.updateEventStartDay=_exports.submitCreateUpdateForm=_exports.getEventById=_exports.getCourseGroupsData=_exports.getCalendarUpcomingData=_exports.getCalendarMonthData=_exports.getCalendarDayData=_exports.deleteSubscription=_exports.deleteEvent=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.deleteEvent=function(eventId){let deleteSeries=arguments.length>1&&void 0!==arguments[1]&&arguments[1];const request={methodname:"core_calendar_delete_calendar_events",args:{events:[{eventid:eventId,repeat:deleteSeries}]}};return _ajax.default.call([request])[0]};_exports.getEventById=eventId=>{const request={methodname:"core_calendar_get_calendar_event_by_id",args:{eventid:eventId}};return _ajax.default.call([request])[0]};_exports.submitCreateUpdateForm=formData=>{const request={methodname:"core_calendar_submit_create_update_form",args:{formdata:formData}};return _ajax.default.call([request])[0]};_exports.getCalendarMonthData=function(year,month,courseId,categoryId,includeNavigation,mini){let day=arguments.length>6&&void 0!==arguments[6]?arguments[6]:1,view=arguments.length>7&&void 0!==arguments[7]?arguments[7]:"month";const request={methodname:"core_calendar_get_calendar_monthly_view",args:{year:year,month:month,courseid:courseId,categoryid:categoryId,includenavigation:includeNavigation,mini:mini,day:day,view:view}};return _ajax.default.call([request])[0]};_exports.getCalendarDayData=(year,month,day,courseId,categoryId)=>{const request={methodname:"core_calendar_get_calendar_day_view",args:{year:year,month:month,day:day,courseid:courseId,categoryid:categoryId}};return _ajax.default.call([request])[0]};_exports.updateEventStartDay=(eventId,dayTimestamp)=>{const request={methodname:"core_calendar_update_event_start_day",args:{eventid:eventId,daytimestamp:dayTimestamp}};return _ajax.default.call([request])[0]};_exports.getCalendarUpcomingData=(courseId,categoryId)=>{const request={methodname:"core_calendar_get_calendar_upcoming_view",args:{courseid:courseId,categoryid:categoryId}};return _ajax.default.call([request])[0]};_exports.getCourseGroupsData=courseId=>{const request={methodname:"core_group_get_course_groups",args:{courseid:courseId}};return _ajax.default.call([request])[0]};_exports.deleteSubscription=subscriptionId=>{const request={methodname:"core_calendar_delete_subscription",args:{subscriptionid:subscriptionId}};return _ajax.default.call([request])[0]}})); //# sourceMappingURL=repository.min.js.mapPK*\ )amd/build/drag_drop_data_store.min.js.mapnu[{"version":3,"file":"drag_drop_data_store.min.js","sources":["../src/drag_drop_data_store.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A javascript module to store calendar drag and drop data.\n *\n * This module is unfortunately required because of the limitations\n * of the HTML5 drag and drop API and it's ability to provide data\n * between the different stages of the drag/drop lifecycle.\n *\n * @module core_calendar/drag_drop_data_store\n * @copyright 2017 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n /* @var {int|null} eventId The id of the event being dragged */\n var eventId = null;\n /* @var {int|null} durationDays How many days the event spans */\n var durationDays = null;\n /* @var {int|null} minTimestart The earliest valid timestart */\n var minTimestart = null;\n /* @var {int|null} maxTimestart The latest valid tiemstart */\n var maxTimestart = null;\n /* @var {string|null} minError Error message for min timestamp violation */\n var minError = null;\n /* @var {string|null} maxError Error message for max timestamp violation */\n var maxError = null;\n\n /**\n * Store the id of the event being dragged.\n *\n * @param {int} id The event id\n */\n var setEventId = function(id) {\n eventId = id;\n };\n\n /**\n * Get the stored event id.\n *\n * @return {int|null}\n */\n var getEventId = function() {\n return eventId;\n };\n\n /**\n * Check if the store has an event id.\n *\n * @return {bool}\n */\n var hasEventId = function() {\n return eventId !== null;\n };\n\n /**\n * Store the duration (in days) of the event being dragged.\n *\n * @param {int} days Number of days the event spans\n */\n var setDurationDays = function(days) {\n durationDays = days;\n };\n\n /**\n * Get the stored number of days.\n *\n * @return {int|null}\n */\n var getDurationDays = function() {\n return durationDays;\n };\n\n /**\n * Store the minimum timestart valid for an event being dragged.\n *\n * @param {int} timestamp The unix timstamp\n */\n var setMinTimestart = function(timestamp) {\n minTimestart = timestamp;\n };\n\n /**\n * Get the minimum valid timestart.\n *\n * @return {int|null}\n */\n var getMinTimestart = function() {\n return minTimestart;\n };\n\n /**\n * Check if a minimum timestamp is set.\n *\n * @return {bool}\n */\n var hasMinTimestart = function() {\n return minTimestart !== null;\n };\n\n /**\n * Store the maximum timestart valid for an event being dragged.\n *\n * @param {int} timestamp The unix timstamp\n */\n var setMaxTimestart = function(timestamp) {\n maxTimestart = timestamp;\n };\n\n /**\n * Get the maximum valid timestart.\n *\n * @return {int|null}\n */\n var getMaxTimestart = function() {\n return maxTimestart;\n };\n\n /**\n * Check if a maximum timestamp is set.\n *\n * @return {bool}\n */\n var hasMaxTimestart = function() {\n return maxTimestart !== null;\n };\n\n /**\n * Store the error string to display if trying to drag an event\n * earlier than the minimum allowed date.\n *\n * @param {string} message The error message\n */\n var setMinError = function(message) {\n minError = message;\n };\n\n /**\n * Get the error message for a minimum time start violation.\n *\n * @return {string|null}\n */\n var getMinError = function() {\n return minError;\n };\n\n /**\n * Store the error string to display if trying to drag an event\n * later than the maximum allowed date.\n *\n * @param {string} message The error message\n */\n var setMaxError = function(message) {\n maxError = message;\n };\n\n /**\n * Get the error message for a maximum time start violation.\n *\n * @return {string|null}\n */\n var getMaxError = function() {\n return maxError;\n };\n\n /**\n * Reset all of the stored values.\n */\n var clearAll = function() {\n setEventId(null);\n setDurationDays(null);\n setMinTimestart(null);\n setMaxTimestart(null);\n setMinError(null);\n setMaxError(null);\n };\n\n return {\n setEventId: setEventId,\n getEventId: getEventId,\n hasEventId: hasEventId,\n setDurationDays: setDurationDays,\n getDurationDays: getDurationDays,\n setMinTimestart: setMinTimestart,\n getMinTimestart: getMinTimestart,\n hasMinTimestart: hasMinTimestart,\n setMaxTimestart: setMaxTimestart,\n getMaxTimestart: getMaxTimestart,\n hasMaxTimestart: hasMaxTimestart,\n setMinError: setMinError,\n getMinError: getMinError,\n setMaxError: setMaxError,\n getMaxError: getMaxError,\n clearAll: clearAll\n };\n});\n"],"names":["define","eventId","durationDays","minTimestart","maxTimestart","minError","maxError","setEventId","id","setDurationDays","days","setMinTimestart","timestamp","setMaxTimestart","setMinError","message","setMaxError","getEventId","hasEventId","getDurationDays","getMinTimestart","hasMinTimestart","getMaxTimestart","hasMaxTimestart","getMinError","getMaxError","clearAll"],"mappings":";;;;;;;;;;;AA0BAA,4CAAO,IAAI,eAEHC,QAAU,KAEVC,aAAe,KAEfC,aAAe,KAEfC,aAAe,KAEfC,SAAW,KAEXC,SAAW,KAOXC,WAAa,SAASC,IACtBP,QAAUO,IA0BVC,gBAAkB,SAASC,MAC3BR,aAAeQ,MAiBfC,gBAAkB,SAASC,WAC3BT,aAAeS,WA0BfC,gBAAkB,SAASD,WAC3BR,aAAeQ,WA2BfE,YAAc,SAASC,SACvBV,SAAWU,SAkBXC,YAAc,SAASD,SACvBT,SAAWS,eAwBR,CACHR,WAAYA,WACZU,WAzIa,kBACNhB,SAyIPiB,WAjIa,kBACM,OAAZjB,SAiIPQ,gBAAiBA,gBACjBU,gBAjHkB,kBACXjB,cAiHPS,gBAAiBA,gBACjBS,gBAjGkB,kBACXjB,cAiGPkB,gBAzFkB,kBACM,OAAjBlB,cAyFPU,gBAAiBA,gBACjBS,gBAzEkB,kBACXlB,cAyEPmB,gBAjEkB,kBACM,OAAjBnB,cAiEPU,YAAaA,YACbU,YAhDc,kBACPnB,UAgDPW,YAAaA,YACbS,YA/Bc,kBACPnB,UA+BPoB,SAzBW,WACXnB,WAAW,MACXE,gBAAgB,MAChBE,gBAAgB,MAChBE,gBAAgB,MAChBC,YAAY,MACZE,YAAY"}PK*\'')amd/build/manage_subscriptions.min.js.mapnu[{"version":3,"file":"manage_subscriptions.min.js","sources":["../src/manage_subscriptions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A module to handle Delete/Update operations of the manage subscription page.\n *\n * @module core_calendar/manage_subscriptions\n * @copyright 2021 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.0\n */\n\nimport * as CalendarSelectors from 'core_calendar/selectors';\nimport * as CalendarRepository from 'core_calendar/repository';\nimport ModalSaveCancel from 'core/modal_save_cancel';\nimport * as ModalEvents from 'core/modal_events';\nimport {exception as displayException, addNotification, fetchNotifications} from 'core/notification';\nimport Prefetch from 'core/prefetch';\nimport {getString} from 'core/str';\nimport {eventTypes} from 'core/local/inplace_editable/events';\n\n/**\n * Get subscription id for given element.\n *\n * @param {HTMLElement} element update/delete link\n * @return {Number}\n */\nconst getSubscriptionId = element => {\n return parseInt(element.closest('tr').dataset.subid);\n};\n\n/**\n * Get subscription name for given element.\n *\n * @param {HTMLElement} element update/delete link\n * @return {String}\n */\nconst getSubscriptionName = element => {\n return element.closest('tr').dataset.subname;\n};\n\n/**\n * Get subscription table row for subscription id.\n *\n * @param {string} subscriptionId Subscription id\n * @return {Element}\n */\nconst getSubscriptionRow = subscriptionId => {\n return document.querySelector(`tr[data-subid=\"${subscriptionId}\"]`);\n};\n\n/**\n * Create modal.\n *\n * @param {HTMLElement} element\n * @param {string} messageCode Message code.\n * @return {promise} Promise for modal\n */\nconst createModal = (element, messageCode) => {\n const subscriptionName = getSubscriptionName(element);\n return ModalSaveCancel.create({\n title: getString('confirmation', 'admin'),\n body: getString(messageCode, 'calendar', subscriptionName),\n buttons: {\n save: getString('yes')\n },\n }).then(modal => {\n modal.getRoot().on(ModalEvents.hidden, () => {\n element.focus();\n });\n modal.show();\n return modal;\n });\n};\n\n/**\n * Response handler for delete action.\n *\n * @param {HTMLElement} element\n * @param {Object} data\n * @return {Promise}\n */\nconst responseHandlerForDelete = async(element, data) => {\n const subscriptionName = getSubscriptionName(element);\n const message = data.status ? await getString('subscriptionremoved', 'calendar', subscriptionName) : data.warnings[0].message;\n const type = data.status ? 'info' : 'error';\n return addNotification({message, type});\n};\n\n/**\n * Register events for update/delete links.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', e => {\n const deleteAction = e.target.closest(CalendarSelectors.actions.deleteSubscription);\n if (deleteAction) {\n e.preventDefault();\n const modalPromise = createModal(deleteAction, 'confirmsubscriptiondelete');\n modalPromise.then(modal => {\n modal.getRoot().on(ModalEvents.save, () => {\n const subscriptionId = getSubscriptionId(deleteAction);\n CalendarRepository.deleteSubscription(subscriptionId).then(data => {\n const response = responseHandlerForDelete(deleteAction, data);\n return response.then(() => {\n const subscriptionRow = getSubscriptionRow(subscriptionId);\n return subscriptionRow.remove();\n });\n }).catch(displayException);\n });\n\n return modal;\n }).catch(displayException);\n }\n });\n\n document.addEventListener(eventTypes.elementUpdated, e => {\n const inplaceEditable = e.target;\n if (inplaceEditable.getAttribute('data-component') == 'core_calendar') {\n fetchNotifications();\n }\n });\n};\n\n/**\n * Initialises.\n */\nexport const init = () => {\n Prefetch.prefetchStrings('moodle', ['yes']);\n Prefetch.prefetchStrings('core_admin', ['confirmation']);\n Prefetch.prefetchStrings('core_calendar', ['confirmsubscriptiondelete', 'subscriptionremoved']);\n registerEventListeners();\n};\n"],"names":["getSubscriptionName","element","closest","dataset","subname","registerEventListeners","document","addEventListener","e","deleteAction","target","CalendarSelectors","actions","deleteSubscription","preventDefault","messageCode","subscriptionName","ModalSaveCancel","create","title","body","buttons","save","then","modal","getRoot","on","ModalEvents","hidden","focus","show","createModal","subscriptionId","parseInt","subid","CalendarRepository","data","response","async","message","status","warnings","type","responseHandlerForDelete","subscriptionRow","querySelector","getSubscriptionRow","remove","catch","displayException","eventTypes","elementUpdated","getAttribute","prefetchStrings"],"mappings":";;;;;;;;+WAiDMA,oBAAsBC,SACjBA,QAAQC,QAAQ,MAAMC,QAAQC,QAsDnCC,uBAAyB,KAC3BC,SAASC,iBAAiB,SAASC,UACzBC,aAAeD,EAAEE,OAAOR,QAAQS,kBAAkBC,QAAQC,uBAC5DJ,aAAc,CACdD,EAAEM,iBAtCM,EAACb,QAASc,qBACpBC,iBAAmBhB,oBAAoBC,gBACtCgB,2BAAgBC,OAAO,CAC1BC,OAAO,kBAAU,eAAgB,SACjCC,MAAM,kBAAUL,YAAa,WAAYC,kBACzCK,QAAS,CACLC,MAAM,kBAAU,UAErBC,MAAKC,QACJA,MAAMC,UAAUC,GAAGC,YAAYC,QAAQ,KACnC3B,QAAQ4B,WAEZL,MAAMM,OACCN,UA0BkBO,CAAYtB,aAAc,6BAClCc,MAAKC,QACdA,MAAMC,UAAUC,GAAGC,YAAYL,MAAM,WAC3BU,eAxEfC,SAwEkDxB,aAxEjCP,QAAQ,MAAMC,QAAQ+B,OAyE9BC,mBAAmBtB,mBAAmBmB,gBAAgBT,MAAKa,aACjDC,SApBGC,OAAMrC,QAASmC,cACtCpB,iBAAmBhB,oBAAoBC,SACvCsC,QAAUH,KAAKI,aAAe,kBAAU,sBAAuB,WAAYxB,kBAAoBoB,KAAKK,SAAS,GAAGF,QAChHG,KAAON,KAAKI,OAAS,OAAS,eAC7B,iCAAgB,CAACD,QAAAA,QAASG,KAAAA,QAgBIC,CAAyBlC,aAAc2B,aACjDC,SAASd,MAAK,WACXqB,gBAzDPZ,CAAAA,gBAChB1B,SAASuC,uCAAgCb,sBAwDAc,CAAmBd,uBACpCY,gBAAgBG,eAE5BC,MAAMC,4BAGNzB,SACRwB,MAAMC,6BAIjB3C,SAASC,iBAAiB2C,mBAAWC,gBAAgB3C,IAEK,iBAD9BA,EAAEE,OACN0C,aAAa,4EASrB,uBACPC,gBAAgB,SAAU,CAAC,0BAC3BA,gBAAgB,aAAc,CAAC,mCAC/BA,gBAAgB,gBAAiB,CAAC,4BAA6B,wBACxEhD"}PK*\L%%amd/build/calendar_view.min.jsnu[/** * This module is responsible for handle calendar day and upcoming view. * * @module core_calendar/calendar_view * @copyright 2017 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("core_calendar/calendar_view",["jquery","core/notification","core_calendar/selectors","core_calendar/events","core_calendar/view_manager","core_calendar/crud"],(function($,Notification,CalendarSelectors,CalendarEvents,CalendarViewManager,CalendarCrud){var registerEventListeners=function(root,type){var body=$("body");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),courseId=selectElement.val();const courseName=$("option:selected",selectElement).text();CalendarViewManager[reloadFunction](root,courseId,null).then((function(){return root.find(CalendarSelectors.courseSelector).val(courseId)})).then((function(){CalendarViewManager.updateUrl("?view="+type+"&course="+courseId),CalendarViewManager.handleCourseChange(Number(courseId),courseName)})).fail(Notification.exception)})),body.on(CalendarEvents.filterChanged,(function(e,data){var daysWithEvent=root.find(CalendarSelectors.eventType[data.type]);1==data.hidden?daysWithEvent.addClass("hidden"):daysWithEvent.removeClass("hidden"),CalendarViewManager.foldDayEvents(root)}));var eventFormPromise=CalendarCrud.registerEventFormModal(root);CalendarCrud.registerEditListeners(root,eventFormPromise)};return{init:function(root,type){let isCalendarBlock=arguments.length>2&&void 0!==arguments[2]&&arguments[2];root=$(root),CalendarViewManager.init(root,type,isCalendarBlock),registerEventListeners(root,type)}}})); //# sourceMappingURL=calendar_view.min.js.mapPK*\ppamd/build/export.min.jsnu[define("core_calendar/export",["exports","core/copy_to_clipboard"],(function(_exports,_copy_to_clipboard){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; /** * A javascript module to enhance the calendar export form. * * @module core_calendar/export * @copyright 2021 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const selectors_copyUrlId="copyexporturl";_exports.init=()=>{const copyUrl=document.getElementById(selectors_copyUrlId);copyUrl.removeAttribute("disabled"),copyUrl.focus()}})); //# sourceMappingURL=export.min.js.mapPK*\]RSS%amd/build/month_view_drag_drop.min.jsnu[/** * A javascript module to handle calendar drag and drop in the calendar * month view. * * @module core_calendar/month_view_drag_drop * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("core_calendar/month_view_drag_drop",["jquery","core/notification","core/str","core_calendar/events","core_calendar/drag_drop_data_store"],(function($,Notification,Str,CalendarEvents,DataStore){var SELECTORS_ROOT="[data-region='calendar']",SELECTORS_DRAGGABLE='[draggable="true"][data-region="event-item"]',SELECTORS_DROP_ZONE='[data-drop-zone="month-view-day"]',SELECTORS_WEEK='[data-region="month-view-week"]',ALL_CLASSES="bg-faded bg-danger text-white bg-primary text-white",registered=!1,getDropZoneFromEvent=function(e){var dropZone=$(e.target).closest(SELECTORS_DROP_ZONE);return dropZone.length?dropZone:null},isValidDropZone=function(dropZone){var dropTimestamp=dropZone.attr("data-day-timestamp"),minTimestart=DataStore.getMinTimestart(),maxTimestart=DataStore.getMaxTimestart();return!(minTimestart&&minTimestart>dropTimestamp)&&!(maxTimestart&&maxTimestart0){var nextDropZone=dropZone.next();if(!nextDropZone.length){var nextWeek=dropZone.closest(SELECTORS_WEEK).next();nextWeek.length&&(nextDropZone=nextWeek.children(SELECTORS_DROP_ZONE).first())}nextDropZone.length&&updateHoverState(nextDropZone,hovered,count)}},updateAllDropZonesState=function(){$(SELECTORS_ROOT).find(SELECTORS_DROP_ZONE).each((function(index,dropZone){dropZone=$(dropZone),isValidDropZone(dropZone)||updateHoverState(dropZone,!1)}))},dragstartHandler=function(e){var draggableElement=$(e.target).closest(SELECTORS_DRAGGABLE);if(draggableElement.length){var eventId=draggableElement.find("[data-event-id]").attr("data-event-id"),minTimestart=draggableElement.attr("data-min-day-timestamp"),maxTimestart=draggableElement.attr("data-max-day-timestamp"),minError=draggableElement.attr("data-min-day-error"),maxError=draggableElement.attr("data-max-day-error"),duration=$(SELECTORS_ROOT+' [data-event-id="'+eventId+'"]').length;DataStore.setEventId(eventId),DataStore.setDurationDays(duration),minTimestart&&DataStore.setMinTimestart(minTimestart),maxTimestart&&DataStore.setMaxTimestart(maxTimestart),minError&&DataStore.setMinError(minError),maxError&&DataStore.setMaxError(maxError),e.dataTransfer.effectAllowed="move",e.dataTransfer.dropEffect="move",e.dataTransfer.setData("text/plain",eventId),e.dropEffect="move",updateAllDropZonesState()}},dragoverHandler=function(e){if(DataStore.hasEventId()){e.preventDefault();var dropZone=getDropZoneFromEvent(e);dropZone&&updateHoverState(dropZone,!0)}},dragleaveHandler=function(e){if(DataStore.hasEventId()){var dropZone=getDropZoneFromEvent(e);dropZone&&(updateHoverState(dropZone,!1),e.preventDefault())}},dropHandler=function(e){if(DataStore.hasEventId()){var dropZone=getDropZoneFromEvent(e);if(!dropZone)return DataStore.clearAll(),void clearAllDropZonesState();if(isValidDropZone(dropZone)){var eventId=DataStore.getEventId(),eventElement=$(SELECTORS_ROOT+' [data-event-id="'+eventId+'"]'),origin=null;eventElement.length&&(origin=eventElement.closest(SELECTORS_DROP_ZONE)),$("body").trigger(CalendarEvents.moveEvent,[eventId,origin,dropZone])}else{var message=function(dropZone){var dropTimestamp=dropZone.attr("data-day-timestamp"),minTimestart=DataStore.getMinTimestart(),maxTimestart=DataStore.getMaxTimestart();return minTimestart&&minTimestart>dropTimestamp?DataStore.getMinError():maxTimestart&&maxTimestart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("core_calendar/crud",["jquery","core/str","core/notification","core/modal_events","core_calendar/modal_event_form","core_calendar/repository","core_calendar/events","core_calendar/modal_delete","core_calendar/selectors","core/pending","core/modal_save_cancel","core/config"],(function($,Str,Notification,ModalEvents,ModalEventForm,CalendarRepository,CalendarEvents,CalendarModalDelete,CalendarSelectors,Pending,ModalSaveCancel,Config){return{registerRemove:function(root){root.on("click",CalendarSelectors.actions.remove,(function(e){var eventSource=$(this).closest(CalendarSelectors.eventItem);!function(eventId,eventTitle,eventCount){var deletePromise,pendingPromise=new Pending("core_calendar/crud:confirmDeletion"),deleteStrings=[{key:"deleteevent",component:"calendar"}],isRepeatedEvent=(eventCount=parseInt(eventCount,10))>1;isRepeatedEvent?(deleteStrings.push({key:"confirmeventseriesdelete",component:"calendar",param:{name:eventTitle,count:eventCount}}),deletePromise=CalendarModalDelete.create()):(deleteStrings.push({key:"confirmeventdelete",component:"calendar",param:eventTitle}),deletePromise=ModalSaveCancel.create());var stringsPromise=Str.get_strings(deleteStrings);$.when(stringsPromise,deletePromise).then((function(strings,deleteModal){return deleteModal.setRemoveOnClose(!0),deleteModal.setTitle(strings[0]),deleteModal.setBody(strings[1]),isRepeatedEvent||deleteModal.setSaveButtonText(strings[0]),deleteModal.show(),deleteModal.getRoot().on(ModalEvents.save,(function(){var pendingPromise=new Pending("calendar/crud:initModal:deletedevent");CalendarRepository.deleteEvent(eventId,!1).then((function(){$("body").trigger(CalendarEvents.deleted,[eventId,!1])})).then(pendingPromise.resolve).catch(Notification.exception)})),deleteModal.getRoot().on(CalendarEvents.deleteAll,(function(){var pendingPromise=new Pending("calendar/crud:initModal:deletedallevent");CalendarRepository.deleteEvent(eventId,!0).then((function(){$("body").trigger(CalendarEvents.deleted,[eventId,!0])})).then(pendingPromise.resolve).catch(Notification.exception)})),deleteModal})).then((function(modal){return pendingPromise.resolve(),modal})).catch(Notification.exception)}(eventSource.data("eventId"),eventSource.data("eventTitle"),eventSource.data("eventCount")),e.preventDefault()}))},registerEditListeners:function(root,eventFormModalPromise){var pendingPromise=new Pending("core_calendar/crud:registerEditListeners");return eventFormModalPromise.then((function(modal){return $("body").on(CalendarEvents.editEvent,(function(e,eventId){var target=root.find("[data-event-id=".concat(eventId,"]")),calendarWrapper=root.find(CalendarSelectors.wrapper);modal.setEventId(eventId),modal.setContextId(calendarWrapper.data("contextId")),modal.setReturnElement(target),modal.show(),e.stopImmediatePropagation()})),modal})).then((function(modal){return pendingPromise.resolve(),modal})).catch(Notification.exception)},registerEventFormModal:function(root){var eventFormPromise=ModalEventForm.create();return root.on("click",CalendarSelectors.actions.create,(function(e){eventFormPromise.then((function(modal){var wrapper=root.find(CalendarSelectors.wrapper),categoryId=wrapper.data("categoryid");const courseId=wrapper.data("courseid");void 0!==categoryId&&courseId!=Config.siteId&&modal.setCategoryId(categoryId);var today=root.find(CalendarSelectors.today),firstDay=root.find(CalendarSelectors.day);!today.length&&firstDay.length&&modal.setStartTime(firstDay.data("newEventTimestamp")),modal.setContextId(wrapper.data("contextId")),modal.setCourseId(wrapper.data("courseid")),modal.show()})).catch(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){modal.setEventId(eventWrapper.data("eventId")),modal.setContextId(calendarWrapper.data("contextId")),modal.setCourseId(eventWrapper.data("courseId")),modal.show(),e.stopImmediatePropagation()})).catch(Notification.exception)})),eventFormPromise}}})); //# sourceMappingURL=crud.min.js.mapPK*\~  amd/build/popover.min.jsnu[define("core_calendar/popover",["theme_boost/bootstrap/popover","core_calendar/selectors"],(function(_popover,CalendarSelectors){var obj; /** * Javascript popover for the `core_calendar` subsystem. * * @module core_calendar/popover * @copyright 2021 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 4.0 */function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}_popover=(obj=_popover)&&obj.__esModule?obj:{default:obj},CalendarSelectors=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CalendarSelectors);const isPopoverConfigured=new Map,showPopover=target=>{const dateContainer=target.closest(CalendarSelectors.elements.dateContainer);if(!isPopoverConfigured.has(dateContainer)){const config={trigger:"manual",placement:"top",html:!0,title:dateContainer.dataset.title,content:()=>{const source=dateContainer.querySelector(CalendarSelectors.elements.dateContent);return source?source.querySelector(".hidden").innerHTML:""},animation:!1};new _popover.default(target,config),isPopoverConfigured.set(dateContainer,!0)}(dateContainer=>"none"===window.getComputedStyle(dateContainer.querySelector(CalendarSelectors.elements.dateContent)).display)(dateContainer)&&(_popover.default.getInstance(target).show(),target.addEventListener("mouseleave",hidePopover),target.addEventListener("focusout",hidePopover),target.addEventListener("click",hidePopover))},hidePopover=e=>{const target=e.target,dateContainer=e.target.closest(CalendarSelectors.elements.dateContainer);if(dateContainer&&isPopoverConfigured.has(dateContainer)){const isTargetActive=target.contains(document.activeElement),isTargetHover=target.matches(":hover"),isTargetClicked=document.activeElement.contains(target);let removeListener=!0;isTargetActive||isTargetHover?isTargetClicked?_popover.default.getOrCreateInstance(document.activeElement).hide():removeListener=!1:_popover.default.getOrCreateInstance(target).hide(),removeListener&&(target.removeEventListener("mouseleave",hidePopover),target.removeEventListener("focusout",hidePopover),target.removeEventListener("click",hidePopover))}};let listenersRegistered=!1;listenersRegistered||((()=>{const showPopoverHandler=e=>{const dayLink=e.target.closest(CalendarSelectors.links.dayLink);dayLink&&(e.preventDefault(),showPopover(dayLink))};document.addEventListener("mouseover",showPopoverHandler),document.addEventListener("focusin",showPopoverHandler)})(),listenersRegistered=!0)})); //# sourceMappingURL=popover.min.js.mapPK*\/dW"amd/build/calendar_view.min.js.mapnu[{"version":3,"file":"calendar_view.min.js","sources":["../src/calendar_view.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module is responsible for handle calendar day and upcoming view.\n *\n * @module core_calendar/calendar_view\n * @copyright 2017 Simey Lameze \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/notification',\n 'core_calendar/selectors',\n 'core_calendar/events',\n 'core_calendar/view_manager',\n 'core_calendar/crud'\n],\nfunction(\n $,\n Notification,\n CalendarSelectors,\n CalendarEvents,\n CalendarViewManager,\n CalendarCrud\n) {\n\n var registerEventListeners = function(root, type) {\n var body = $('body');\n\n CalendarCrud.registerRemove(root);\n\n var reloadFunction = 'reloadCurrent' + type.charAt(0).toUpperCase() + type.slice(1);\n\n body.on(CalendarEvents.created, function() {\n CalendarViewManager[reloadFunction](root);\n });\n body.on(CalendarEvents.deleted, function() {\n CalendarViewManager[reloadFunction](root);\n });\n body.on(CalendarEvents.updated, function() {\n CalendarViewManager[reloadFunction](root);\n });\n\n root.on('change', CalendarSelectors.courseSelector, function() {\n var selectElement = $(this);\n var courseId = selectElement.val();\n const courseName = $(\"option:selected\", selectElement).text();\n CalendarViewManager[reloadFunction](root, courseId, null)\n .then(function() {\n // We need to get the selector again because the content has changed.\n return root.find(CalendarSelectors.courseSelector).val(courseId);\n })\n .then(function() {\n CalendarViewManager.updateUrl('?view=' + type + '&course=' + courseId);\n CalendarViewManager.handleCourseChange(Number(courseId), courseName);\n return;\n })\n .fail(Notification.exception);\n });\n\n body.on(CalendarEvents.filterChanged, function(e, data) {\n var daysWithEvent = root.find(CalendarSelectors.eventType[data.type]);\n if (data.hidden == true) {\n daysWithEvent.addClass('hidden');\n } else {\n daysWithEvent.removeClass('hidden');\n }\n CalendarViewManager.foldDayEvents(root);\n });\n\n var eventFormPromise = CalendarCrud.registerEventFormModal(root);\n CalendarCrud.registerEditListeners(root, eventFormPromise);\n };\n\n return {\n init: function(root, type, isCalendarBlock = false) {\n root = $(root);\n\n CalendarViewManager.init(root, type, isCalendarBlock);\n registerEventListeners(root, type, isCalendarBlock);\n }\n };\n });\n"],"names":["define","$","Notification","CalendarSelectors","CalendarEvents","CalendarViewManager","CalendarCrud","registerEventListeners","root","type","body","registerRemove","reloadFunction","charAt","toUpperCase","slice","on","created","deleted","updated","courseSelector","selectElement","this","courseId","val","courseName","text","then","find","updateUrl","handleCourseChange","Number","fail","exception","filterChanged","e","data","daysWithEvent","eventType","hidden","addClass","removeClass","foldDayEvents","eventFormPromise","registerEventFormModal","registerEditListeners","init","isCalendarBlock"],"mappings":";;;;;;;AAsBAA,qCAAO,CACH,SACA,oBACA,0BACA,uBACA,6BACA,uBAEJ,SACIC,EACAC,aACAC,kBACAC,eACAC,oBACAC,kBAGQC,uBAAyB,SAASC,KAAMC,UACpCC,KAAOT,EAAE,QAEbK,aAAaK,eAAeH,UAExBI,eAAiB,gBAAkBH,KAAKI,OAAO,GAAGC,cAAgBL,KAAKM,MAAM,GAEjFL,KAAKM,GAAGZ,eAAea,SAAS,WAC5BZ,oBAAoBO,gBAAgBJ,SAExCE,KAAKM,GAAGZ,eAAec,SAAS,WAC5Bb,oBAAoBO,gBAAgBJ,SAExCE,KAAKM,GAAGZ,eAAee,SAAS,WAC5Bd,oBAAoBO,gBAAgBJ,SAGxCA,KAAKQ,GAAG,SAAUb,kBAAkBiB,gBAAgB,eAC5CC,cAAgBpB,EAAEqB,MAClBC,SAAWF,cAAcG,YACvBC,WAAaxB,EAAE,kBAAmBoB,eAAeK,OACvDrB,oBAAoBO,gBAAgBJ,KAAMe,SAAU,MAC/CI,MAAK,kBAEKnB,KAAKoB,KAAKzB,kBAAkBiB,gBAAgBI,IAAID,aAE1DI,MAAK,WACFtB,oBAAoBwB,UAAU,SAAWpB,KAAO,WAAac,UAC7DlB,oBAAoByB,mBAAmBC,OAAOR,UAAWE,eAG5DO,KAAK9B,aAAa+B,cAG3BvB,KAAKM,GAAGZ,eAAe8B,eAAe,SAASC,EAAGC,UAC1CC,cAAgB7B,KAAKoB,KAAKzB,kBAAkBmC,UAAUF,KAAK3B,OAC5C,GAAf2B,KAAKG,OACLF,cAAcG,SAAS,UAEvBH,cAAcI,YAAY,UAE9BpC,oBAAoBqC,cAAclC,aAGlCmC,iBAAmBrC,aAAasC,uBAAuBpC,MAC3DF,aAAauC,sBAAsBrC,KAAMmC,yBAGtC,CACHG,KAAM,SAAStC,KAAMC,UAAMsC,wEACvBvC,KAAOP,EAAEO,MAETH,oBAAoByC,KAAKtC,KAAMC,KAAMsC,iBACrCxC,uBAAuBC,KAAMC"}PK*\3X66amd/build/repository.min.js.mapnu[{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A javascript module to handle calendar ajax actions.\n *\n * @module core_calendar/repository\n * @copyright 2017 Simey Lameze \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Ajax from 'core/ajax';\n\n/**\n * Delete a calendar event.\n *\n * @method deleteEvent\n * @param {number} eventId The event id.\n * @param {boolean} deleteSeries Whether to delete all events in the series\n * @return {promise} Resolved with requested calendar event\n */\nexport const deleteEvent = (eventId, deleteSeries = false) => {\n const request = {\n methodname: 'core_calendar_delete_calendar_events',\n args: {\n events: [{\n eventid: eventId,\n repeat: deleteSeries,\n }]\n }\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get a calendar event by id.\n *\n * @method getEventById\n * @param {number} eventId The event id.\n * @return {promise} Resolved with requested calendar event\n */\nexport const getEventById = (eventId) => {\n\n const request = {\n methodname: 'core_calendar_get_calendar_event_by_id',\n args: {\n eventid: eventId\n }\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Submit the form data for the event form.\n *\n * @method submitCreateUpdateForm\n * @param {string} formData The URL encoded values from the form\n * @return {promise} Resolved with the new or edited event\n */\nexport const submitCreateUpdateForm = (formData) => {\n const request = {\n methodname: 'core_calendar_submit_create_update_form',\n args: {\n formdata: formData\n }\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get calendar data for the month view.\n *\n * @method getCalendarMonthData\n * @param {number} year Year\n * @param {number} month Month\n * @param {number} courseId The course id.\n * @param {number} categoryId The category id.\n * @param {boolean} includeNavigation Whether to include navigation.\n * @param {boolean} mini Whether the month is in mini view.\n * @param {number} day Day (optional)\n * @param {string} view The calendar view mode.\n * @return {promise} Resolved with the month view data.\n */\nexport const getCalendarMonthData = (year, month, courseId, categoryId, includeNavigation, mini, day = 1, view = 'month') => {\n const request = {\n methodname: 'core_calendar_get_calendar_monthly_view',\n args: {\n year,\n month,\n courseid: courseId,\n categoryid: categoryId,\n includenavigation: includeNavigation,\n mini,\n day,\n view,\n }\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get calendar data for the day view.\n *\n * @method getCalendarDayData\n * @param {number} year Year\n * @param {number} month Month\n * @param {number} day Day\n * @param {number} courseId The course id.\n * @param {number} categoryId The id of the category whose events are shown\n * @return {promise} Resolved with the day view data.\n */\nexport const getCalendarDayData = (year, month, day, courseId, categoryId) => {\n const request = {\n methodname: 'core_calendar_get_calendar_day_view',\n args: {\n year,\n month,\n day,\n courseid: courseId,\n categoryid: categoryId,\n }\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Change the start day for the given event id. The day timestamp\n * only has to be any time during the target day because only the\n * date information is extracted, the time of the day is ignored.\n *\n * @param {int} eventId The id of the event to update\n * @param {int} dayTimestamp A timestamp for some time during the target day\n * @return {promise}\n */\nexport const updateEventStartDay = (eventId, dayTimestamp) => {\n const request = {\n methodname: 'core_calendar_update_event_start_day',\n args: {\n eventid: eventId,\n daytimestamp: dayTimestamp\n }\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get calendar upcoming data.\n *\n * @method getCalendarUpcomingData\n * @param {number} courseId The course id.\n * @param {number} categoryId The category id.\n * @return {promise} Resolved with the month view data.\n */\nexport const getCalendarUpcomingData = (courseId, categoryId) => {\n const request = {\n methodname: 'core_calendar_get_calendar_upcoming_view',\n args: {\n courseid: courseId,\n categoryid: categoryId,\n }\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get the groups by course id.\n *\n * @param {Number} courseId The course id to fetch the groups from.\n * @return {promise} Resolved with the course groups.\n */\nexport const getCourseGroupsData = (courseId) => {\n const request = {\n methodname: 'core_group_get_course_groups',\n args: {\n courseid: courseId\n }\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Delete calendar subscription by id.\n *\n * @param {Number} subscriptionId The subscription id\n * @return {promise}\n */\nexport const deleteSubscription = (subscriptionId) => {\n const request = {\n methodname: 'core_calendar_delete_subscription',\n args: {\n subscriptionid: subscriptionId\n }\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["eventId","deleteSeries","request","methodname","args","events","eventid","repeat","Ajax","call","formData","formdata","year","month","courseId","categoryId","includeNavigation","mini","day","view","courseid","categoryid","includenavigation","dayTimestamp","daytimestamp","subscriptionId","subscriptionid"],"mappings":";;;;;;;yYAgC2B,SAACA,aAASC,2EAC3BC,QAAU,CACZC,WAAY,uCACZC,KAAM,CACFC,OAAQ,CAAC,CACLC,QAASN,QACTO,OAAQN,wBAKbO,cAAKC,KAAK,CAACP,UAAU,0BAUHF,gBAEnBE,QAAU,CACZC,WAAY,yCACZC,KAAM,CACFE,QAASN,iBAIVQ,cAAKC,KAAK,CAACP,UAAU,oCAUOQ,iBAC7BR,QAAU,CACZC,WAAY,0CACZC,KAAM,CACFO,SAAUD,kBAIXF,cAAKC,KAAK,CAACP,UAAU,kCAiBI,SAACU,KAAMC,MAAOC,SAAUC,WAAYC,kBAAmBC,UAAMC,2DAAM,EAAGC,4DAAO,cACvGjB,QAAU,CACZC,WAAY,0CACZC,KAAM,CACFQ,KAAAA,KACAC,MAAAA,MACAO,SAAUN,SACVO,WAAYN,WACZO,kBAAmBN,kBACnBC,KAAAA,KACAC,IAAAA,IACAC,KAAAA,cAIDX,cAAKC,KAAK,CAACP,UAAU,gCAcE,CAACU,KAAMC,MAAOK,IAAKJ,SAAUC,oBACrDb,QAAU,CACZC,WAAY,sCACZC,KAAM,CACFQ,KAAAA,KACAC,MAAAA,MACAK,IAAAA,IACAE,SAAUN,SACVO,WAAYN,oBAIbP,cAAKC,KAAK,CAACP,UAAU,iCAYG,CAACF,QAASuB,sBACnCrB,QAAU,CACZC,WAAY,uCACZC,KAAM,CACFE,QAASN,QACTwB,aAAcD,sBAIff,cAAKC,KAAK,CAACP,UAAU,qCAWO,CAACY,SAAUC,oBACxCb,QAAU,CACZC,WAAY,2CACZC,KAAM,CACFgB,SAAUN,SACVO,WAAYN,oBAIbP,cAAKC,KAAK,CAACP,UAAU,iCASIY,iBAC1BZ,QAAU,CACZC,WAAY,+BACZC,KAAM,CACFgB,SAAUN,kBAIXN,cAAKC,KAAK,CAACP,UAAU,gCASGuB,uBACzBvB,QAAU,CACZC,WAAY,oCACZC,KAAM,CACFsB,eAAgBD,wBAIjBjB,cAAKC,KAAK,CAACP,UAAU"}PK*\0TgDgD%amd/build/modal_event_form.min.js.mapnu[{"version":3,"file":"modal_event_form.min.js","sources":["../src/modal_event_form.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the logic for the quick add or update event modal.\n *\n * @module core_calendar/modal_event_form\n * @copyright 2017 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport * as CustomEvents from 'core/custom_interaction_events';\nimport Modal from 'core/modal';\nimport * as FormEvents from 'core_form/events';\nimport CalendarEvents from './events';\nimport * as Str from 'core/str';\nimport * as Notification from 'core/notification';\nimport * as Fragment from 'core/fragment';\nimport * as Repository from 'core_calendar/repository';\n\nconst SELECTORS = {\n SAVE_BUTTON: '[data-action=\"save\"]',\n LOADING_ICON_CONTAINER: '[data-region=\"loading-icon-container\"]',\n};\n\nexport default class ModalEventForm extends Modal {\n static TYPE = 'core_calendar-modal_event_form';\n static TEMPLATE = 'calendar/modal_event_form';\n\n /**\n * Constructor for the Modal.\n *\n * @param {object} root The root jQuery element for the modal\n */\n constructor(root) {\n super(root);\n\n this.eventId = null;\n this.startTime = null;\n this.courseId = null;\n this.categoryId = null;\n this.contextId = null;\n this.reloadingBody = false;\n this.reloadingTitle = false;\n this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n super.configure(modalConfig);\n }\n\n /**\n * Set the context id to the given value.\n *\n * @method setContextId\n * @param {Number} id The event id\n */\n setContextId(id) {\n this.contextId = id;\n }\n\n /**\n * Retrieve the current context id, if any.\n *\n * @method getContextId\n * @return {Number|null} The event id\n */\n getContextId() {\n return this.contextId;\n }\n\n /**\n * Set the course id to the given value.\n *\n * @method setCourseId\n * @param {Number} id The event id\n */\n setCourseId(id) {\n this.courseId = id;\n }\n\n /**\n * Retrieve the current course id, if any.\n *\n * @method getCourseId\n * @return {Number|null} The event id\n */\n getCourseId() {\n return this.courseId;\n }\n\n /**\n * Set the category id to the given value.\n *\n * @method setCategoryId\n * @param {Number} id The event id\n */\n setCategoryId(id) {\n this.categoryId = id;\n }\n\n /**\n * Retrieve the current category id, if any.\n *\n * @method getCategoryId\n * @return {Number|null} The event id\n */\n getCategoryId() {\n return this.categoryId;\n }\n\n /**\n * Check if the modal has an course id.\n *\n * @method hasCourseId\n * @return {bool}\n */\n hasCourseId() {\n return this.courseId !== null;\n }\n\n /**\n * Check if the modal has an category id.\n *\n * @method hasCategoryId\n * @return {bool}\n */\n hasCategoryId() {\n return this.categoryId !== null;\n }\n\n /**\n * Set the event id to the given value.\n *\n * @method setEventId\n * @param {Number} id The event id\n */\n setEventId(id) {\n this.eventId = id;\n }\n\n /**\n * Retrieve the current event id, if any.\n *\n * @method getEventId\n * @return {Number|null} The event id\n */\n getEventId() {\n return this.eventId;\n }\n\n /**\n * Check if the modal has an event id.\n *\n * @method hasEventId\n * @return {bool}\n */\n hasEventId() {\n return this.eventId !== null;\n }\n\n /**\n * Set the start time to the given value.\n *\n * @method setStartTime\n * @param {Number} time The start time\n */\n setStartTime(time) {\n this.startTime = time;\n }\n\n /**\n * Retrieve the current start time, if any.\n *\n * @method getStartTime\n * @return {Number|null} The start time\n */\n getStartTime() {\n return this.startTime;\n }\n\n /**\n * Check if the modal has start time.\n *\n * @method hasStartTime\n * @return {bool}\n */\n hasStartTime() {\n return this.startTime !== null;\n }\n\n /**\n * Get the form element from the modal.\n *\n * @method getForm\n * @return {object}\n */\n getForm() {\n return this.getBody().find('form');\n }\n\n /**\n * Disable the buttons in the footer.\n *\n * @method disableButtons\n */\n disableButtons() {\n this.saveButton.prop('disabled', true);\n }\n\n /**\n * Enable the buttons in the footer.\n *\n * @method enableButtons\n */\n enableButtons() {\n this.saveButton.prop('disabled', false);\n }\n\n /**\n * Reload the title for the modal to the appropriate value\n * depending on whether we are creating a new event or\n * editing an existing event.\n *\n * @method reloadTitleContent\n * @return {object} A promise resolved with the new title text\n */\n reloadTitleContent() {\n if (this.reloadingTitle) {\n return this.titlePromise;\n }\n\n this.reloadingTitle = true;\n\n if (this.hasEventId()) {\n this.titlePromise = Str.get_string('editevent', 'calendar');\n } else {\n this.titlePromise = Str.get_string('newevent', 'calendar');\n }\n\n this.titlePromise.then((string) => {\n this.setTitle(string);\n return string;\n })\n .catch(Notification.exception)\n .always(() => {\n this.reloadingTitle = false;\n return;\n });\n\n return this.titlePromise;\n }\n\n /**\n * Send a request to the server to get the event_form in a fragment\n * and render the result in the body of the modal.\n *\n * If serialised form data is provided then it will be sent in the\n * request to the server to have the form rendered with the data. This\n * is used when the form had a server side error and we need the server\n * to re-render it for us to display the error to the user.\n *\n * @method reloadBodyContent\n * @param {string} formData The serialised form data\n * @return {object} A promise resolved with the fragment html and js from\n */\n reloadBodyContent(formData) {\n if (this.reloadingBody) {\n return this.bodyPromise;\n }\n\n this.reloadingBody = true;\n this.disableButtons();\n\n const args = {};\n\n if (this.hasEventId()) {\n args.eventid = this.getEventId();\n }\n\n if (this.hasStartTime()) {\n args.starttime = this.getStartTime();\n }\n\n if (this.hasCourseId()) {\n args.courseid = this.getCourseId();\n }\n\n if (this.hasCategoryId()) {\n args.categoryid = this.getCategoryId();\n }\n\n if (typeof formData !== 'undefined') {\n args.formdata = formData;\n }\n\n this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', this.getContextId(), args);\n\n this.setBody(this.bodyPromise);\n\n this.bodyPromise.then(() => {\n this.enableButtons();\n return;\n })\n .catch(Notification.exception)\n .always(() => {\n this.reloadingBody = false;\n return;\n });\n\n return this.bodyPromise;\n }\n\n /**\n * Reload both the title and body content.\n *\n * @method reloadAllContent\n * @return {object} promise\n */\n reloadAllContent() {\n return $.when(this.reloadTitleContent(), this.reloadBodyContent());\n }\n\n /**\n * Kick off a reload the modal content before showing it. This\n * is to allow us to re-use the same modal for creating and\n * editing different events within the page.\n *\n * We do the reload when showing the modal rather than hiding it\n * to save a request to the server if the user closes the modal\n * and never re-opens it.\n *\n * @method show\n */\n show() {\n this.reloadAllContent();\n super.show(this);\n }\n\n /**\n * Clear the event id from the modal when it's closed so\n * that it is loaded fresh next time it's displayed.\n *\n * The event id will be set by the calling code if it wants\n * to edit a specific event.\n *\n * @method hide\n */\n hide() {\n super.hide(this);\n this.setEventId(null);\n this.setStartTime(null);\n this.setCourseId(null);\n this.setCategoryId(null);\n }\n\n /**\n * Get the serialised form data.\n *\n * @method getFormData\n * @return {string} serialised form data\n */\n getFormData() {\n return this.getForm().serialize();\n }\n\n /**\n * Send the form data to the server to create or update\n * an event.\n *\n * If there is a server side validation error then we re-request the\n * rendered form (with the data) from the server in order to get the\n * server side errors to display.\n *\n * On success the modal is hidden and the page is reloaded so that the\n * new event will display.\n *\n * @method save\n * @return {object} A promise\n */\n save() {\n const loadingContainer = this.saveButton.find(SELECTORS.LOADING_ICON_CONTAINER);\n\n // Now the change events have run, see if there are any \"invalid\" form fields.\n const invalid = this.getForm().find('[aria-invalid=\"true\"]');\n\n // If we found invalid fields, focus on the first one and do not submit via ajax.\n if (invalid.length) {\n invalid.first().focus();\n return Promise.resolve();\n }\n\n loadingContainer.removeClass('hidden');\n this.disableButtons();\n\n const formData = this.getFormData();\n // Send the form data to the server for processing.\n return Repository.submitCreateUpdateForm(formData)\n .then((response) => {\n if (response.validationerror) {\n // If there was a server side validation error then\n // we need to re-request the rendered form from the server\n // in order to display the error for the user.\n this.reloadBodyContent(formData);\n return;\n } else {\n // Check whether this was a new event or not.\n // The hide function unsets the form data so grab this before the hide.\n const isExisting = this.hasEventId();\n\n // No problemo! Our work here is done.\n this.hide();\n\n // Trigger the appropriate calendar event so that the view can be updated.\n if (isExisting) {\n $('body').trigger(CalendarEvents.updated, [response.event]);\n } else {\n $('body').trigger(CalendarEvents.created, [response.event]);\n }\n }\n\n return;\n })\n .catch(Notification.exception)\n .always(() => {\n // Regardless of success or error we should always stop\n // the loading icon and re-enable the buttons.\n loadingContainer.addClass('hidden');\n this.enableButtons();\n\n return;\n });\n }\n\n /**\n * Set up all of the event handling for the modal.\n *\n * @method registerEventListeners\n * @fires event:uploadStarted\n * @fires event:formSubmittedByJavascript\n */\n registerEventListeners() {\n // Apply parent event listeners.\n super.registerEventListeners(this);\n\n // When the user clicks the save button we trigger the form submission. We need to\n // trigger an actual submission because there is some JS code in the form that is\n // listening for this event and doing some stuff (e.g. saving draft areas etc).\n this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, (e, data) => {\n this.getForm().submit();\n data.originalEvent.preventDefault();\n e.stopPropagation();\n });\n\n // Catch the submit event before it is actually processed by the browser and\n // prevent the submission. We'll take it from here.\n this.getModal().on('submit', (e) => {\n FormEvents.notifyFormSubmittedByJavascript(this.getForm()[0]);\n\n this.save();\n\n // Stop the form from actually submitting and prevent it's\n // propagation because we have already handled the event.\n e.preventDefault();\n e.stopPropagation();\n });\n }\n}\n\nModalEventForm.registerModalType();\n"],"names":["SELECTORS","ModalEventForm","Modal","constructor","root","eventId","startTime","courseId","categoryId","contextId","reloadingBody","reloadingTitle","saveButton","this","getFooter","find","configure","modalConfig","large","setContextId","id","getContextId","setCourseId","getCourseId","setCategoryId","getCategoryId","hasCourseId","hasCategoryId","setEventId","getEventId","hasEventId","setStartTime","time","getStartTime","hasStartTime","getForm","getBody","disableButtons","prop","enableButtons","reloadTitleContent","titlePromise","Str","get_string","then","string","setTitle","catch","Notification","exception","always","reloadBodyContent","formData","bodyPromise","args","eventid","starttime","courseid","categoryid","formdata","Fragment","loadFragment","setBody","reloadAllContent","$","when","show","hide","getFormData","serialize","save","loadingContainer","invalid","length","first","focus","Promise","resolve","removeClass","Repository","submitCreateUpdateForm","response","validationerror","isExisting","trigger","CalendarEvents","updated","event","created","addClass","registerEventListeners","getModal","on","CustomEvents","events","activate","e","data","submit","originalEvent","preventDefault","stopPropagation","FormEvents","notifyFormSubmittedByJavascript","registerModalType"],"mappings":"u8DAiCMA,sBACW,uBADXA,iCAEsB,+CAGPC,uBAAuBC,eASxCC,YAAYC,YACFA,WAEDC,QAAU,UACVC,UAAY,UACZC,SAAW,UACXC,WAAa,UACbC,UAAY,UACZC,eAAgB,OAChBC,gBAAiB,OACjBC,WAAaC,KAAKC,YAAYC,KAAKf,uBAG5CgB,UAAUC,aACNA,YAAYC,OAAQ,QACdF,UAAUC,aASpBE,aAAaC,SACJX,UAAYW,GASrBC,sBACWR,KAAKJ,UAShBa,YAAYF,SACHb,SAAWa,GASpBG,qBACWV,KAAKN,SAShBiB,cAAcJ,SACLZ,WAAaY,GAStBK,uBACWZ,KAAKL,WAShBkB,qBAC6B,OAAlBb,KAAKN,SAShBoB,uBAC+B,OAApBd,KAAKL,WAShBoB,WAAWR,SACFf,QAAUe,GASnBS,oBACWhB,KAAKR,QAShByB,oBAC4B,OAAjBjB,KAAKR,QAShB0B,aAAaC,WACJ1B,UAAY0B,KASrBC,sBACWpB,KAAKP,UAShB4B,sBAC8B,OAAnBrB,KAAKP,UAShB6B,iBACWtB,KAAKuB,UAAUrB,KAAK,QAQ/BsB,sBACSzB,WAAW0B,KAAK,YAAY,GAQrCC,qBACS3B,WAAW0B,KAAK,YAAY,GAWrCE,4BACQ3B,KAAKF,sBAIJA,gBAAiB,EAElBE,KAAKiB,kBACAW,aAAeC,IAAIC,WAAW,YAAa,iBAE3CF,aAAeC,IAAIC,WAAW,WAAY,iBAG9CF,aAAaG,MAAMC,cACfC,SAASD,QACPA,UAEVE,MAAMC,aAAaC,WACnBC,QAAO,UACCvC,gBAAiB,MAjBfE,KAAK4B,aAqCpBU,kBAAkBC,aACVvC,KAAKH,qBACEG,KAAKwC,iBAGX3C,eAAgB,OAChB2B,uBAECiB,KAAO,UAETzC,KAAKiB,eACLwB,KAAKC,QAAU1C,KAAKgB,cAGpBhB,KAAKqB,iBACLoB,KAAKE,UAAY3C,KAAKoB,gBAGtBpB,KAAKa,gBACL4B,KAAKG,SAAW5C,KAAKU,eAGrBV,KAAKc,kBACL2B,KAAKI,WAAa7C,KAAKY,sBAGH,IAAb2B,WACPE,KAAKK,SAAWP,eAGfC,YAAcO,SAASC,aAAa,WAAY,aAAchD,KAAKQ,eAAgBiC,WAEnFQ,QAAQjD,KAAKwC,kBAEbA,YAAYT,MAAK,UACbL,mBAGRQ,MAAMC,aAAaC,WACnBC,QAAO,UACCxC,eAAgB,KAIlBG,KAAKwC,YAShBU,0BACWC,gBAAEC,KAAKpD,KAAK2B,qBAAsB3B,KAAKsC,qBAclDe,YACSH,yBACCG,KAAKrD,MAYfsD,aACUA,KAAKtD,WACNe,WAAW,WACXG,aAAa,WACbT,YAAY,WACZE,cAAc,MASvB4C,qBACWvD,KAAKsB,UAAUkC,YAiB1BC,aACUC,iBAAmB1D,KAAKD,WAAWG,KAAKf,kCAGxCwE,QAAU3D,KAAKsB,UAAUpB,KAAK,4BAGhCyD,QAAQC,cACRD,QAAQE,QAAQC,QACTC,QAAQC,UAGnBN,iBAAiBO,YAAY,eACxBzC,uBAECe,SAAWvC,KAAKuD,qBAEfW,WAAWC,uBAAuB5B,UACpCR,MAAMqC,cACCA,SAASC,qBAIJ/B,kBAAkBC,cAEpB,OAGG+B,WAAatE,KAAKiB,kBAGnBqC,OAGDgB,+BACE,QAAQC,QAAQC,iBAAeC,QAAS,CAACL,SAASM,4BAElD,QAAQH,QAAQC,iBAAeG,QAAS,CAACP,SAASM,YAM/DxC,MAAMC,aAAaC,WACnBC,QAAO,KAGJqB,iBAAiBkB,SAAS,eACrBlD,mBAajBmD,+BAEUA,uBAAuB7E,WAKxB8E,WAAWC,GAAGC,aAAaC,OAAOC,SAAU/F,uBAAuB,CAACgG,EAAGC,aACnE9D,UAAU+D,SACfD,KAAKE,cAAcC,iBACnBJ,EAAEK,0BAKDV,WAAWC,GAAG,UAAWI,IAC1BM,WAAWC,gCAAgC1F,KAAKsB,UAAU,SAErDmC,OAIL0B,EAAEI,iBACFJ,EAAEK,6EAxbOpG,sBACH,kDADGA,0BAEC,6BA2btBA,eAAeuG"}PK*\eamd/build/events.min.js.mapnu[{"version":3,"file":"events.min.js","sources":["../src/events.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the events the calendar component can fire.\n *\n * @module core_calendar/events\n * @copyright 2017 Simey Lameze \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n created: 'calendar-events:created',\n deleted: 'calendar-events:deleted',\n deleteAll: 'calendar-events:delete_all',\n updated: 'calendar-events:updated',\n editEvent: 'calendar-events:edit_event',\n editActionEvent: 'calendar-events:edit_action_event',\n eventMoved: 'calendar-events:event_moved',\n dayChanged: 'calendar-events:day_changed',\n monthChanged: 'calendar-events:month_changed',\n moveEvent: 'calendar-events:move_event',\n filterChanged: 'calendar-events:filter_changed',\n courseChanged: 'calendar-events:course_changed',\n viewUpdated: 'calendar-events:view_updated',\n};\n"],"names":["created","deleted","deleteAll","updated","editEvent","editActionEvent","eventMoved","dayChanged","monthChanged","moveEvent","filterChanged","courseChanged","viewUpdated"],"mappings":"sKAsBe,CACXA,QAAS,0BACTC,QAAS,0BACTC,UAAW,6BACXC,QAAS,0BACTC,UAAW,6BACXC,gBAAiB,oCACjBC,WAAY,8BACZC,WAAY,8BACZC,aAAc,gCACdC,UAAW,6BACXC,cAAe,iCACfC,cAAe,iCACfC,YAAa"}PK*\UF>>amd/build/view_manager.min.jsnu[define("core_calendar/view_manager",["exports","jquery","core/templates","core/notification","core_calendar/repository","core_calendar/events","core_calendar/selectors","core/modal_events","core_calendar/summary_modal","core/custom_interaction_events","core/str","core/pending","core/prefetch","core/url","core/config"],(function(_exports,_jquery,_templates,_notification,CalendarRepository,_events,CalendarSelectors,_modal_events,_summary_modal,_custom_interaction_events,_str,_pending,_prefetch,_url,_config){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * A javascript module to handler calendar view changes. * * @module core_calendar/view_manager * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.updateUrl=_exports.reloadCurrentUpcoming=_exports.reloadCurrentMonth=_exports.reloadCurrentDay=_exports.registerEventListenersForMonthDetailed=_exports.refreshMonthContent=_exports.refreshDayContent=_exports.init=_exports.handleCourseChange=_exports.foldDayEvents=_exports.changeMonth=_exports.changeDay=void 0,_jquery=_interopRequireDefault(_jquery),_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),CalendarRepository=_interopRequireWildcard(CalendarRepository),_events=_interopRequireDefault(_events),CalendarSelectors=_interopRequireWildcard(CalendarSelectors),_modal_events=_interopRequireDefault(_modal_events),_summary_modal=_interopRequireDefault(_summary_modal),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_pending=_interopRequireDefault(_pending),_url=_interopRequireDefault(_url),_config=_interopRequireDefault(_config);const foldDayEvents=()=>{const days=(0,_jquery.default)(CalendarSelectors.elements.monthDetailed).find(CalendarSelectors.day);0!==days.length&&days.each((function(){const dayContainer=(0,_jquery.default)(this),eventsSelector="".concat(CalendarSelectors.elements.dateContent," ul li[data-event-eventtype]"),filteredEventsSelector="".concat(CalendarSelectors.elements.dateContent,' ul li[data-event-filtered="true"]'),moreEventsSelector="".concat(CalendarSelectors.elements.dateContent,' [data-action="view-more-events"]'),events=dayContainer.find(eventsSelector);if(0===events.length)return;const numberOfFiltered=dayContainer.find(filteredEventsSelector).length,numberOfEvents=events.length-numberOfFiltered;let count=1;events.each((function(){const event=(0,_jquery.default)(this);"true"!==event.attr("data-event-filtered")?count>5-(5===numberOfEvents?0:1)?(event.attr("data-event-folded","true"),event.hide()):(event.attr("data-event-folded","false"),event.show(),count++):event.attr("data-event-folded","false")}));const moreEventsLink=dayContainer.find(moreEventsSelector);if(numberOfEvents>5){const numberOfHiddenEvents=numberOfEvents-5+1;moreEventsLink.show(),(0,_str.getString)("moreevents","calendar",numberOfHiddenEvents).then((str=>{const link=moreEventsLink.find("strong a");return moreEventsLink.attr("data-event-folded","false"),link.text(str),str})).catch(_notification.default.exception)}else moreEventsLink.hide()}))};_exports.foldDayEvents=foldDayEvents;const registerEventListenersForMonthDetailed=pendingId=>{const events="".concat(_events.default.viewUpdated);(0,_jquery.default)("body").on(events,(function(e){foldDayEvents()})),foldDayEvents(),(0,_jquery.default)("body").on(_events.default.filterChanged,(function(e,data){const root=(0,_jquery.default)(CalendarSelectors.elements.monthDetailed),pending=new _pending.default(pendingId),target=root.find(CalendarSelectors.eventType[data.type]),transitionPromise=_jquery.default.Deferred();data.hidden?transitionPromise.then((function(){return target.attr("data-event-filtered","true"),target.hide().promise()})).fail():transitionPromise.then((function(){return target.attr("data-event-filtered","false"),target.show().promise()})).fail(),transitionPromise.then((function(){foldDayEvents()})).always(pending.resolve).fail(),transitionPromise.resolve()}))};_exports.registerEventListenersForMonthDetailed=registerEventListenersForMonthDetailed;const refreshMonthContent=function(root,year,month,courseId,categoryId){let target=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null,template=arguments.length>6&&void 0!==arguments[6]?arguments[6]:"",day=arguments.length>7&&void 0!==arguments[7]?arguments[7]:1;startLoading(root),target=target||root.find(CalendarSelectors.wrapper),template=template||root.attr("data-template"),M.util.js_pending([root.get("id"),year,month,courseId].join("-"));const includenavigation=root.data("includenavigation"),mini=root.data("mini"),viewMode=target.data("view");return CalendarRepository.getCalendarMonthData(year,month,courseId,categoryId,includenavigation,mini,day,viewMode).then((context=>_templates.default.render(template,context))).then(((html,js)=>_templates.default.replaceNode(target,html,js))).then((()=>{document.querySelector("body").dispatchEvent(new CustomEvent(_events.default.viewUpdated))})).always((()=>(M.util.js_complete([root.get("id"),year,month,courseId].join("-")),stopLoading(root)))).fail(_notification.default.exception)};_exports.refreshMonthContent=refreshMonthContent;const changeMonth=function(root,url,year,month,courseId,categoryId){let day=arguments.length>6&&void 0!==arguments[6]?arguments[6]:1,isCalendarBlock=arguments.length>7&&void 0!==arguments[7]&&arguments[7];return refreshMonthContent(root,year,month,courseId,categoryId,null,"",day).then((function(){url.length&&"#"!==url&&!isCalendarBlock&&updateUrl(url);for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++)args[_key]=arguments[_key];return args})).then((function(){(0,_jquery.default)("body").trigger(_events.default.monthChanged,[year,month,courseId,categoryId,day,isCalendarBlock]);for(var _len2=arguments.length,args=new Array(_len2),_key2=0;_key2<_len2;_key2++)args[_key2]=arguments[_key2];return args}))};_exports.changeMonth=changeMonth;_exports.reloadCurrentMonth=function(root){let courseId=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,categoryId=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;const year=root.find(CalendarSelectors.wrapper).data("year"),month=root.find(CalendarSelectors.wrapper).data("month"),day=root.find(CalendarSelectors.wrapper).data("day");return courseId=courseId||root.find(CalendarSelectors.wrapper).data("courseid"),categoryId=categoryId||root.find(CalendarSelectors.wrapper).data("categoryid"),refreshMonthContent(root,year,month,courseId,categoryId,null,"",day).then((function(){(0,_jquery.default)("body").trigger(_events.default.courseChanged,[year,month,courseId,categoryId]);for(var _len3=arguments.length,args=new Array(_len3),_key3=0;_key3<_len3;_key3++)args[_key3]=arguments[_key3];return args}))};const refreshDayContent=function(root,year,month,day,courseId,categoryId){let target=arguments.length>6&&void 0!==arguments[6]?arguments[6]:null,template=arguments.length>7&&void 0!==arguments[7]?arguments[7]:"",isCalendarBlock=arguments.length>8&&void 0!==arguments[8]&&arguments[8];startLoading(root),target&&0!=target.length||(target=root.find(CalendarSelectors.wrapper)),template=template||root.attr("data-template"),M.util.js_pending([root.get("id"),year,month,day,courseId,categoryId].join("-"));const includenavigation=root.data("includenavigation");return CalendarRepository.getCalendarDayData(year,month,day,courseId,categoryId,includenavigation).then((context=>(context.viewingday=!0,context.showviewselector=!0,context.iscalendarblock=isCalendarBlock,_templates.default.render(template,context)))).then(((html,js)=>_templates.default.replaceNode(target,html,js))).then((()=>{document.querySelector("body").dispatchEvent(new CustomEvent(_events.default.viewUpdated))})).always((()=>(M.util.js_complete([root.get("id"),year,month,day,courseId,categoryId].join("-")),stopLoading(root)))).fail(_notification.default.exception)};_exports.refreshDayContent=refreshDayContent;_exports.reloadCurrentDay=function(root){let courseId=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,categoryId=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;const wrapper=root.find(CalendarSelectors.wrapper),year=wrapper.data("year"),month=wrapper.data("month"),day=wrapper.data("day");return courseId=courseId||root.find(CalendarSelectors.wrapper).data("courseid"),categoryId=categoryId||root.find(CalendarSelectors.wrapper).data("categoryid"),refreshDayContent(root,year,month,day,courseId,categoryId)};const changeDay=function(root,url,year,month,day,courseId,categoryId){let isCalendarBlock=arguments.length>7&&void 0!==arguments[7]&&arguments[7];return refreshDayContent(root,year,month,day,courseId,categoryId,null,"",isCalendarBlock).then((function(){url.length&&"#"!==url&&!isCalendarBlock&&updateUrl(url);for(var _len4=arguments.length,args=new Array(_len4),_key4=0;_key4<_len4;_key4++)args[_key4]=arguments[_key4];return args})).then((function(){(0,_jquery.default)("body").trigger(_events.default.dayChanged,[year,month,courseId,categoryId,isCalendarBlock]);for(var _len5=arguments.length,args=new Array(_len5),_key5=0;_key5<_len5;_key5++)args[_key5]=arguments[_key5];return args}))};_exports.changeDay=changeDay;const updateUrl=url=>{document.getElementById(CalendarSelectors.fullCalendarView)&&window.history.pushState({},"",url)};_exports.updateUrl=updateUrl;const startLoading=root=>{root.find(CalendarSelectors.containers.loadingIcon).removeClass("hidden")},stopLoading=root=>{root.find(CalendarSelectors.containers.loadingIcon).addClass("hidden")},reloadCurrentUpcoming=function(root){let courseId=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,categoryId=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,target=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,template=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"";return startLoading(root),target=target||root.find(CalendarSelectors.wrapper),template=template||root.attr("data-template"),courseId=courseId||root.find(CalendarSelectors.wrapper).data("courseid"),categoryId=categoryId||root.find(CalendarSelectors.wrapper).data("categoryid"),CalendarRepository.getCalendarUpcomingData(courseId,categoryId).then((context=>(context.viewingupcoming=!0,context.showviewselector=!0,_templates.default.render(template,context)))).then(((html,js)=>_templates.default.replaceNode(target,html,js))).then((()=>{document.querySelector("body").dispatchEvent(new CustomEvent(_events.default.viewUpdated))})).always((function(){return stopLoading(root)})).fail(_notification.default.exception)};_exports.reloadCurrentUpcoming=reloadCurrentUpcoming;const renderEventSummaryModal=eventId=>{const pendingPromise=new _pending.default("core_calendar/view_manager:renderEventSummaryModal");return CalendarRepository.getEventById(eventId).then((getEventResponse=>{if(!getEventResponse.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+eventId);return getEventResponse.event})).then((eventData=>{const modalParams={title:eventData.name,body:_templates.default.render("core_calendar/event_summary_body",eventData),templateContext:{canedit:eventData.canedit,candelete:eventData.candelete,headerclasses:(eventType=eventData.normalisedeventtype,"calendar_event_"+eventType),isactionevent:eventData.isactionevent,url:eventData.url,action:eventData.action}};var eventType;return _summary_modal.default.create(modalParams)})).then((modal=>(modal.getRoot().on(_modal_events.default.hidden,(function(){modal.destroy()})),modal.show(),modal))).then((modal=>(pendingPromise.resolve(),modal))).catch(_notification.default.exception)};_exports.init=(root,view,isCalendarBlock)=>{(0,_prefetch.prefetchStrings)("calendar",["moreevents"]),foldDayEvents(),((root,isCalendarBlock)=>{(root=(0,_jquery.default)(root)).on("click",CalendarSelectors.links.eventLink,(e=>{const target=e.target;let eventLink=null,eventId=null;const pendingPromise=new _pending.default("core_calendar/view_manager:eventLink:click");eventLink=target.matches(CalendarSelectors.actions.viewEvent)?target:target.closest(CalendarSelectors.actions.viewEvent),eventId=eventLink?eventLink.dataset.eventId:target.querySelector(CalendarSelectors.actions.viewEvent).dataset.eventId,eventId?(e.preventDefault(),e.stopPropagation(),renderEventSummaryModal(eventId).then(pendingPromise.resolve).catch()):pendingPromise.resolve()})),root.on("click",CalendarSelectors.links.navLink,(e=>{const wrapper=root.find(CalendarSelectors.wrapper),view=wrapper.data("view"),courseId=wrapper.data("courseid"),categoryId=wrapper.data("categoryid"),link=e.currentTarget;"month"===view||"monthblock"===view?(changeMonth(root,link.href,link.dataset.year,link.dataset.month,courseId,categoryId,link.dataset.day,isCalendarBlock),e.preventDefault()):"day"===view&&(changeDay(root,link.href,link.dataset.year,link.dataset.month,link.dataset.day,courseId,categoryId,isCalendarBlock),e.preventDefault())}));const viewSelector=root.find(CalendarSelectors.viewSelector);_custom_interaction_events.default.define(viewSelector,[_custom_interaction_events.default.events.activate]),viewSelector.on(_custom_interaction_events.default.events.activate,(e=>{e.preventDefault();const option=e.target;if(option.classList.contains("active"))return;const view=option.dataset.view,year=option.dataset.year,month=option.dataset.month,day=option.dataset.day,courseId=option.dataset.courseid,categoryId=option.dataset.categoryid;"month"==view?refreshMonthContent(root,year,month,courseId,categoryId,root,"core_calendar/calendar_month",day).then((()=>{updateUrl("?view=month&course="+courseId)})).fail(_notification.default.exception):"day"==view?refreshDayContent(root,year,month,day,courseId,categoryId,root,"core_calendar/calendar_day").then((()=>{updateUrl("?view=day&course="+courseId)})).fail(_notification.default.exception):"upcoming"==view&&reloadCurrentUpcoming(root,courseId,categoryId,root,"core_calendar/calendar_upcoming").then((()=>{updateUrl("?view=upcoming&course="+courseId)})).fail(_notification.default.exception)}))})(root,isCalendarBlock);const calendarTable=root.find(CalendarSelectors.elements.monthDetailed);if(calendarTable.length){"month-detailed-".concat(calendarTable.id,"-filterChanged");registerEventListenersForMonthDetailed(calendarTable)}};_exports.handleCourseChange=async(courseId,courseName)=>{document.querySelector(CalendarSelectors.viewSelector+" ul").querySelectorAll("li a").forEach((element=>{element.setAttribute("data-courseid",courseId)}));const calendar=await(0,_str.getString)("calendar","calendar"),pageHeaderHeadingsElement=document.querySelector(CalendarSelectors.pageHeaderHeadings),courseUrl=_url.default.relativeUrl("/course/view.php",{id:courseId});courseId!==_config.default.siteId?pageHeaderHeadingsElement.innerHTML=calendar+': '+courseName+"":pageHeaderHeadingsElement.innerHTML=calendar}})); //# sourceMappingURL=view_manager.min.js.mapPK*\!amd/build/modal_delete.min.js.mapnu[{"version":3,"file":"modal_delete.min.js","sources":["../src/modal_delete.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the logic for the delete modal.\n *\n * @module core_calendar/modal_delete\n * @copyright 2017 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport * as CustomEvents from 'core/custom_interaction_events';\nimport Modal from 'core/modal';\nimport ModalEvents from 'core/modal_events';\nimport CalendarEvents from './events';\n\nconst SELECTORS = {\n DELETE_ONE_BUTTON: '[data-action=\"deleteone\"]',\n DELETE_ALL_BUTTON: '[data-action=\"deleteall\"]',\n CANCEL_BUTTON: '[data-action=\"cancel\"]',\n};\n\n/**\n * Constructor for the Modal.\n *\n * @class\n * @param {object} root The root jQuery element for the modal\n */\nexport default class ModalDelete extends Modal {\n static TYPE = 'core_calendar-modal_delete';\n static TEMPLATE = 'calendar/event_delete_modal';\n\n constructor(root) {\n super(root);\n this.setRemoveOnClose(true);\n }\n\n /**\n * Set up all of the event handling for the modal.\n *\n * @method registerEventListeners\n */\n registerEventListeners() {\n // Apply parent event listeners.\n super.registerEventListeners(this);\n\n this.getModal().on(CustomEvents.events.activate, SELECTORS.DELETE_ONE_BUTTON, (e, data) => {\n const saveEvent = $.Event(ModalEvents.save);\n this.getRoot().trigger(saveEvent, this);\n\n if (!saveEvent.isDefaultPrevented()) {\n this.hide();\n data.originalEvent.preventDefault();\n }\n });\n\n this.getModal().on(CustomEvents.events.activate, SELECTORS.DELETE_ALL_BUTTON, (e, data) => {\n const saveEvent = $.Event(CalendarEvents.deleteAll);\n this.getRoot().trigger(saveEvent, this);\n\n if (!saveEvent.isDefaultPrevented()) {\n this.hide();\n data.originalEvent.preventDefault();\n }\n });\n\n this.getModal().on(CustomEvents.events.activate, SELECTORS.CANCEL_BUTTON, (e, data) => {\n const cancelEvent = $.Event(ModalEvents.cancel);\n this.getRoot().trigger(cancelEvent, this);\n\n if (!cancelEvent.isDefaultPrevented()) {\n this.hide();\n data.originalEvent.preventDefault();\n }\n });\n }\n}\n\nModalDelete.registerModalType();\n"],"names":["SELECTORS","ModalDelete","Modal","constructor","root","setRemoveOnClose","registerEventListeners","this","getModal","on","CustomEvents","events","activate","e","data","saveEvent","$","Event","ModalEvents","save","getRoot","trigger","isDefaultPrevented","hide","originalEvent","preventDefault","CalendarEvents","deleteAll","cancelEvent","cancel","registerModalType"],"mappings":"6nDA6BMA,4BACiB,4BADjBA,4BAEiB,4BAFjBA,wBAGa,+BASEC,oBAAoBC,eAIrCC,YAAYC,YACFA,WACDC,kBAAiB,GAQ1BC,+BAEUA,uBAAuBC,WAExBC,WAAWC,GAAGC,aAAaC,OAAOC,SAAUZ,6BAA6B,CAACa,EAAGC,cACxEC,UAAYC,gBAAEC,MAAMC,sBAAYC,WACjCC,UAAUC,QAAQN,UAAWR,MAE7BQ,UAAUO,4BACNC,OACLT,KAAKU,cAAcC,0BAItBjB,WAAWC,GAAGC,aAAaC,OAAOC,SAAUZ,6BAA6B,CAACa,EAAGC,cACxEC,UAAYC,gBAAEC,MAAMS,gBAAeC,gBACpCP,UAAUC,QAAQN,UAAWR,MAE7BQ,UAAUO,4BACNC,OACLT,KAAKU,cAAcC,0BAItBjB,WAAWC,GAAGC,aAAaC,OAAOC,SAAUZ,yBAAyB,CAACa,EAAGC,cACpEc,YAAcZ,gBAAEC,MAAMC,sBAAYW,aACnCT,UAAUC,QAAQO,YAAarB,MAE/BqB,YAAYN,4BACRC,OACLT,KAAKU,cAAcC,0EA5CdxB,mBACH,8CADGA,uBAEC,+BAgDtBA,YAAY6B"}PK*\ ;;amd/build/selectors.min.js.mapnu[{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * CSS selectors for the calendar.\n *\n * @module core_calendar/selectors\n * @copyright 2017 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n eventFilterItem: \"[data-action='filter-event-type']\",\n eventType: {\n site: \"[data-eventtype-site]\",\n category: \"[data-eventtype-category]\",\n course: \"[data-eventtype-course]\",\n group: \"[data-eventtype-group]\",\n user: \"[data-eventtype-user]\",\n other: \"[data-eventtype-other]\",\n },\n popoverType: {\n site: \"[data-popover-eventtype-site]\",\n category: \"[data-popover-eventtype-category]\",\n course: \"[data-popover-eventtype-course]\",\n group: \"[data-popover-eventtype-group]\",\n user: \"[data-popover-eventtype-user]\",\n other: \"[data-popover-eventtype-other]\",\n },\n calendarPeriods: {\n month: \"[data-period='month']\",\n },\n courseSelector: 'select[name=\"course\"]',\n viewSelector: 'div[data-region=\"view-selector\"]',\n actions: {\n create: '[data-action=\"new-event-button\"]',\n edit: '[data-action=\"edit\"]',\n remove: '[data-action=\"delete\"]',\n viewEvent: '[data-action=\"view-event\"]',\n deleteSubscription: '[data-action=\"delete-subscription\"]',\n },\n elements: {\n courseSelector: 'select[name=\"course\"]',\n dateContainer: '.clickable.hasevent',\n dateContent: '[data-region=\"day-content\"]',\n monthDetailed: '.calendarmonth.calendartable',\n },\n today: '.today',\n day: '[data-region=\"day\"]',\n calendarMain: '[data-region=\"calendar\"]',\n wrapper: '.calendarwrapper',\n eventItem: '[data-type=\"event\"]',\n links: {\n navLink: '.calendarwrapper .arrow_link',\n eventLink: \"[data-region='event-item']\",\n miniDayLink: \"[data-region='mini-day-link']\",\n dayLink: \"[data-action='view-day-link']\",\n },\n containers: {\n loadingIcon: '[data-region=\"overlay-icon-container\"]',\n },\n mainCalendar: '.maincalendar .heightcontainer',\n fullCalendarView: 'page-calendar-view',\n pageHeaderHeadings: '.page-header-headings h1',\n };\n});\n"],"names":["define","eventFilterItem","eventType","site","category","course","group","user","other","popoverType","calendarPeriods","month","courseSelector","viewSelector","actions","create","edit","remove","viewEvent","deleteSubscription","elements","dateContainer","dateContent","monthDetailed","today","day","calendarMain","wrapper","eventItem","links","navLink","eventLink","miniDayLink","dayLink","containers","loadingIcon","mainCalendar","fullCalendarView","pageHeaderHeadings"],"mappings":";;;;;;;AAsBAA,iCAAO,IAAI,iBACA,CACHC,gBAAiB,oCACjBC,UAAW,CACPC,KAAM,wBACNC,SAAU,4BACVC,OAAQ,0BACRC,MAAO,yBACPC,KAAM,wBACNC,MAAO,0BAEXC,YAAa,CACTN,KAAM,gCACNC,SAAU,oCACVC,OAAQ,kCACRC,MAAO,iCACPC,KAAM,gCACNC,MAAO,kCAEXE,gBAAiB,CACbC,MAAO,yBAEXC,eAAgB,wBAChBC,aAAc,mCACdC,QAAS,CACLC,OAAQ,mCACRC,KAAM,uBACNC,OAAQ,yBACRC,UAAW,6BACXC,mBAAoB,uCAExBC,SAAU,CACNR,eAAgB,wBAChBS,cAAe,sBACfC,YAAa,8BACbC,cAAe,gCAEnBC,MAAO,SACPC,IAAK,sBACLC,aAAc,2BACdC,QAAS,mBACTC,UAAW,sBACXC,MAAO,CACHC,QAAS,+BACTC,UAAW,6BACXC,YAAa,gCACbC,QAAS,iCAEbC,WAAY,CACRC,YAAa,0CAEjBC,aAAc,iCACdC,iBAAkB,qBAClBC,mBAAoB"}PK*\TvDDamd/build/event_form.min.js.mapnu[{"version":3,"file":"event_form.min.js","sources":["../src/event_form.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A javascript module to enhance the event form.\n *\n * @module core_calendar/event_form\n * @copyright 2017 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core_calendar/repository', 'core/notification'], function($, CalendarRepository, Notification) {\n\n var SELECTORS = {\n EVENT_GROUP_COURSE_ID: '[name=\"groupcourseid\"]',\n EVENT_GROUP_ID: '[name=\"groupid\"]',\n SELECT_OPTION: 'option'\n };\n\n /**\n * Listen for when the user changes the group course when configuring\n * a group event and filter the options in the group select to only\n * show the groups available within the course the user has selected.\n *\n * @method addCourseGroupSelectListeners\n * @param {object} formElement The root form element\n */\n var addCourseGroupSelectListeners = function(formElement) {\n var courseGroupSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID);\n\n var loadGroupSelectOptions = function(groups) {\n var groupSelect = formElement.find(SELECTORS.EVENT_GROUP_ID),\n groupSelectOptions = groupSelect.find(SELECTORS.SELECT_OPTION),\n courseGroups = $(groups);\n\n // Let's clear all options first.\n groupSelectOptions.remove();\n groupSelect.prop(\"disabled\", false);\n courseGroups.each(function(id, group) {\n $(groupSelect).append($(\"\").attr(\"value\", group.id).text(group.name));\n });\n };\n\n // If the user choose a course in the selector do a WS request to get groups.\n courseGroupSelect.on('change', function() {\n var courseId = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID).val();\n if (isNaN(courseId) || courseId <= 0) {\n loadGroupSelectOptions([]);\n } else {\n CalendarRepository.getCourseGroupsData(courseId)\n .then(function(groups) {\n return loadGroupSelectOptions(groups);\n })\n .catch(Notification.exception);\n }\n });\n };\n\n /**\n * Initialise all of the form enhancements.\n *\n * @method init\n * @param {string} formId The value of the form's id attribute\n */\n var init = function(formId) {\n var formElement = $('#' + formId);\n addCourseGroupSelectListeners(formElement);\n };\n\n return {\n init: init,\n };\n});\n"],"names":["define","$","CalendarRepository","Notification","SELECTORS","init","formId","formElement","courseGroupSelect","find","loadGroupSelectOptions","groups","groupSelect","groupSelectOptions","courseGroups","remove","prop","each","id","group","append","attr","text","name","on","courseId","val","isNaN","getCourseGroupsData","then","catch","exception","addCourseGroupSelectListeners"],"mappings":";;;;;;;AAsBAA,kCAAO,CAAC,SAAU,2BAA4B,sBAAsB,SAASC,EAAGC,mBAAoBC,kBAE5FC,gCACuB,yBADvBA,yBAEgB,mBAFhBA,wBAGe,eAqDZ,CACHC,KANO,SAASC,SArCgB,SAASC,iBACrCC,kBAAoBD,YAAYE,KAAKL,iCAErCM,uBAAyB,SAASC,YAC9BC,YAAcL,YAAYE,KAAKL,0BAC/BS,mBAAqBD,YAAYH,KAAKL,yBACtCU,aAAeb,EAAEU,QAGrBE,mBAAmBE,SACnBH,YAAYI,KAAK,YAAY,GAC7BF,aAAaG,MAAK,SAASC,GAAIC,OAC3BlB,EAAEW,aAAaQ,OAAOnB,EAAE,qBAAqBoB,KAAK,QAASF,MAAMD,IAAII,KAAKH,MAAMI,WAKxFf,kBAAkBgB,GAAG,UAAU,eACvBC,SAAWlB,YAAYE,KAAKL,iCAAiCsB,MAC7DC,MAAMF,WAAaA,UAAY,EAC/Bf,uBAAuB,IAEvBR,mBAAmB0B,oBAAoBH,UAClCI,MAAK,SAASlB,eACJD,uBAAuBC,WAEjCmB,MAAM3B,aAAa4B,cAahCC,CADkB/B,EAAE,IAAMK"}PK*\#4ɲamd/build/summary_modal.min.jsnu[define("core_calendar/summary_modal",["exports","jquery","core/custom_interaction_events","core/modal","./events","core_calendar/crud","core/modal_events"],(function(_exports,_jquery,CustomEvents,_modal,_events,CalendarCrud,ModalEvents){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),CustomEvents=_interopRequireWildcard(CustomEvents),_modal=_interopRequireDefault(_modal),_events=_interopRequireDefault(_events),CalendarCrud=_interopRequireWildcard(CalendarCrud),ModalEvents=_interopRequireWildcard(ModalEvents);const SELECTORS_ROOT="[data-region='summary-modal-container']",SELECTORS_EDIT_BUTTON='[data-action="edit"]',SELECTORS_DELETE_BUTTON='[data-action="delete"]';class ModalEventSummary extends _modal.default{getEditButton(){return void 0===this.editButton&&(this.editButton=this.getFooter().find(SELECTORS_EDIT_BUTTON)),this.editButton}getDeleteButton(){return void 0===this.deleteButton&&(this.deleteButton=this.getFooter().find(SELECTORS_DELETE_BUTTON)),this.deleteButton}getEventId(){return this.getBody().find(SELECTORS_ROOT).attr("data-event-id")}getEventTitle(){return this.getBody().find(SELECTORS_ROOT).attr("data-event-title")}getEventCount(){return this.getBody().find(SELECTORS_ROOT).attr("data-event-count")}getEditUrl(){return this.getBody().find(SELECTORS_ROOT).attr("data-edit-url")}isActionEvent(){return"true"==this.getBody().find(SELECTORS_ROOT).attr("data-action-event")}registerEventListeners(){super.registerEventListeners(this),M.util.js_pending("core_calendar/summary_modal:registerEventListeners:bodyRendered"),this.getRoot().on(ModalEvents.bodyRendered,function(){this.getModal().data({eventTitle:this.getEventTitle(),eventId:this.getEventId(),eventCount:this.getEventCount()}).attr("data-type","event"),CalendarCrud.registerRemove(this.getModal()),M.util.js_complete("core_calendar/summary_modal:registerEventListeners:bodyRendered")}.bind(this)),(0,_jquery.default)("body").on(_events.default.deleted,function(){this.hide()}.bind(this)),CustomEvents.define(this.getEditButton(),[CustomEvents.events.activate]),this.getEditButton().on(CustomEvents.events.activate,function(e,data){this.isActionEvent()?(0,_jquery.default)("body").trigger(_events.default.editActionEvent,[this.getEditUrl()]):(0,_jquery.default)("body").trigger(_events.default.editEvent,[this.getEventId()]),this.hide(),e.preventDefault(),e.stopPropagation(),data.originalEvent.preventDefault(),data.originalEvent.stopPropagation()}.bind(this))}}return _exports.default=ModalEventSummary,_defineProperty(ModalEventSummary,"TEMPLATE","core_calendar/event_summary_modal"),_defineProperty(ModalEventSummary,"TYPE","core_calendar-event_summary"),ModalEventSummary.registerModalType(),_exports.default})); //# sourceMappingURL=summary_modal.min.js.mapPK*\amd/build/calendar.min.jsnu[/** * This module is the highest level module for the calendar. It is * responsible for initialising all of the components required for * the calendar to run. It also coordinates the interaction between * components by listening for and responding to different events * triggered within the calendar UI. * * @module core_calendar/calendar * @copyright 2017 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("core_calendar/calendar",["jquery","core/templates","core/notification","core_calendar/repository","core_calendar/events","core_calendar/view_manager","core_calendar/crud","core_calendar/selectors","core/url","core/str"],(function($,Templates,Notification,CalendarRepository,CalendarEvents,CalendarViewManager,CalendarCrud,CalendarSelectors,Url,Str){var SELECTORS_DAY="[data-region='day']",SELECTORS_DAY_CONTENT="[data-region='day-content']",SELECTORS_LOADING_ICON=".loading-icon",SELECTORS_VIEW_DAY_LINK="[data-action='view-day-link']",SELECTORS_CALENDAR_MONTH_WRAPPER=".calendarwrapper",SELECTORS_SCREEN_READER_ANNOUNCEMENTS=".calendar-announcements",SELECTORS_CURRENT_MONTH=".calendar-controls .current",handleMoveEvent=function(e,eventId,originElement,destinationElement){var originTimestamp=null,destinationTimestamp=destinationElement.attr("data-day-timestamp");originElement&&(originTimestamp=originElement.attr("data-day-timestamp")),originElement&&originTimestamp==destinationTimestamp||Templates.render("core/loading",{}).then((function(html,js){destinationElement.find(SELECTORS_DAY_CONTENT).addClass("hidden"),Templates.appendNodeContents(destinationElement,html,js),originElement&&(originElement.find(SELECTORS_DAY_CONTENT).addClass("hidden"),Templates.appendNodeContents(originElement,html,js))})).then((function(){return CalendarRepository.updateEventStartDay(eventId,destinationTimestamp)})).then((function(){$("body").trigger(CalendarEvents.eventMoved,[eventId,originElement,destinationElement])})).always((function(){var destinationLoadingElement=destinationElement.find(SELECTORS_LOADING_ICON);if(destinationElement.find(SELECTORS_DAY_CONTENT).removeClass("hidden"),Templates.replaceNode(destinationLoadingElement,"",""),originElement){var originLoadingElement=originElement.find(SELECTORS_LOADING_ICON);originElement.find(SELECTORS_DAY_CONTENT).removeClass("hidden"),Templates.replaceNode(originLoadingElement,"","")}})).catch(Notification.exception)},registerEventListeners=function(root,isCalendarBlock){const viewingFullCalendar=document.getElementById(CalendarSelectors.fullCalendarView);root.on("click",SELECTORS_VIEW_DAY_LINK,(function(e){var dayLink=$(e.target).closest(SELECTORS_VIEW_DAY_LINK),year=dayLink.data("year"),month=dayLink.data("month"),day=dayLink.data("day"),courseId=dayLink.data("courseid"),categoryId=dayLink.data("categoryid");const urlParams={view:"day",time:dayLink.data("timestamp"),course:courseId};if(viewingFullCalendar){const urlParamString=Object.entries(urlParams).map((_ref=>{let[key,value]=_ref;return"".concat(encodeURIComponent(key),"=").concat(encodeURIComponent(value))})).join("&");CalendarViewManager.refreshDayContent(root,year,month,day,courseId,categoryId,root,"core_calendar/calendar_day",isCalendarBlock).then((function(){e.preventDefault(),isCalendarBlock||CalendarViewManager.updateUrl("?"+urlParamString)})).catch(Notification.exception)}else window.location.assign(Url.relativeUrl("calendar/view.php",urlParams))})),root.on("change",CalendarSelectors.elements.courseSelector,(function(){var selectElement=$(this),courseId=selectElement.val();const courseName=$("option:selected",selectElement).text();CalendarViewManager.reloadCurrentMonth(root,courseId,null).then((function(){return root.find(CalendarSelectors.elements.courseSelector).val(courseId)})).then((function(){CalendarViewManager.updateUrl("?view=month&course="+courseId),CalendarViewManager.handleCourseChange(Number(courseId),courseName)})).catch(Notification.exception)}));var eventFormPromise=CalendarCrud.registerEventFormModal(root),contextId=$(SELECTORS_CALENDAR_MONTH_WRAPPER).data("context-id");!function(root,eventFormModalPromise){var body=$("body");body.on(CalendarEvents.created,(function(){CalendarViewManager.reloadCurrentMonth(root)})),body.on(CalendarEvents.deleted,(function(){CalendarViewManager.reloadCurrentMonth(root)})),body.on(CalendarEvents.updated,(function(){CalendarViewManager.reloadCurrentMonth(root)})),body.on(CalendarEvents.editActionEvent,(function(e,url){window.location.assign(url)})),body.on(CalendarEvents.moveEvent,handleMoveEvent),body.on(CalendarEvents.eventMoved,(function(){CalendarViewManager.reloadCurrentMonth(root)})),body.on(CalendarEvents.monthChanged,root,(async function(){const monthName=body.find(SELECTORS_CURRENT_MONTH).text(),monthAnnoucement=await Str.get_string("newmonthannouncement","calendar",monthName);body.find(SELECTORS_SCREEN_READER_ANNOUNCEMENTS).html(monthAnnoucement)})),CalendarCrud.registerEditListeners(root,eventFormModalPromise)}(root,eventFormPromise),contextId&&root.on("click",SELECTORS_DAY,(function(e){var target=$(e.target);const displayingSmallBlockCalendar="side-pre"===root.parents("aside").data("blockregion");if(!viewingFullCalendar&&displayingSmallBlockCalendar){const dateContainer=target.closest(SELECTORS_DAY),courseId=target.closest(CalendarSelectors.wrapper).data("courseid"),params={view:"day",time:dateContainer.data("day-timestamp"),course:courseId};window.location.assign(Url.relativeUrl("calendar/view.php",params))}else{if(!target.closest(SELECTORS_VIEW_DAY_LINK).length){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");void 0!==categoryId&&modal.setCategoryId(categoryId),modal.setContextId(wrapper.data("contextId")),modal.setStartTime(startTime),modal.show()})).catch(Notification.exception)}}e.preventDefault()}))};return{init:function(root){let isCalendarBlock=arguments.length>1&&void 0!==arguments[1]&&arguments[1];root=$(root),CalendarViewManager.init(root,"month",isCalendarBlock),registerEventListeners(root,isCalendarBlock)}}})); //# sourceMappingURL=calendar.min.js.mapPK*\q[[amd/build/event_form.min.jsnu[/** * A javascript module to enhance the event form. * * @module core_calendar/event_form * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("core_calendar/event_form",["jquery","core_calendar/repository","core/notification"],(function($,CalendarRepository,Notification){var SELECTORS_EVENT_GROUP_COURSE_ID='[name="groupcourseid"]',SELECTORS_EVENT_GROUP_ID='[name="groupid"]',SELECTORS_SELECT_OPTION="option";return{init:function(formId){!function(formElement){var courseGroupSelect=formElement.find(SELECTORS_EVENT_GROUP_COURSE_ID),loadGroupSelectOptions=function(groups){var groupSelect=formElement.find(SELECTORS_EVENT_GROUP_ID),groupSelectOptions=groupSelect.find(SELECTORS_SELECT_OPTION),courseGroups=$(groups);groupSelectOptions.remove(),groupSelect.prop("disabled",!1),courseGroups.each((function(id,group){$(groupSelect).append($("").attr("value",group.id).text(group.name))}))};courseGroupSelect.on("change",(function(){var courseId=formElement.find(SELECTORS_EVENT_GROUP_COURSE_ID).val();isNaN(courseId)||courseId<=0?loadGroupSelectOptions([]):CalendarRepository.getCourseGroupsData(courseId).then((function(groups){return loadGroupSelectOptions(groups)})).catch(Notification.exception)}))}($("#"+formId))}}})); //# sourceMappingURL=event_form.min.js.mapPK*\ÜStt$amd/build/calendar_filter.min.js.mapnu[{"version":3,"file":"calendar_filter.min.js","sources":["../src/calendar_filter.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module is responsible for the calendar filter.\n *\n * @module core_calendar/calendar_filter\n * @copyright 2017 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core_calendar/selectors',\n 'core_calendar/events',\n 'core/str',\n 'core/templates',\n],\nfunction(\n $,\n CalendarSelectors,\n CalendarEvents,\n Str,\n Templates\n) {\n\n var registerEventListeners = function(root) {\n root.on('click', CalendarSelectors.eventFilterItem, function(e) {\n var target = $(e.currentTarget);\n\n toggleFilter(target);\n\n e.preventDefault();\n });\n\n $('body').on(CalendarEvents.viewUpdated, function() {\n var filters = root.find(CalendarSelectors.eventFilterItem);\n\n filters.each(function(i, filter) {\n filter = $(filter);\n if (filter.data('eventtype-hidden')) {\n var data = getFilterData(filter);\n fireFilterChangedEvent(data);\n }\n });\n });\n };\n\n var toggleFilter = function(target) {\n var data = getFilterData(target);\n\n // Toggle the hidden. We need to render the template before we change the value.\n data.hidden = !data.hidden;\n\n M.util.js_pending(\"core_calendar/calendar_filter:toggleFilter\");\n return Str.get_string('eventtype' + data.eventtype, 'calendar')\n .then(function(nameStr) {\n data.name = nameStr;\n data.icon = true;\n data.key = 'i/' + data.eventtype + 'event';\n data.component = 'core';\n\n return data;\n })\n .then(function(context) {\n return Templates.render('core_calendar/event_filter_key', context);\n })\n .then(function(html, js) {\n return Templates.replaceNode(target, html, js);\n })\n .then(function() {\n fireFilterChangedEvent(data);\n M.util.js_complete(\"core_calendar/calendar_filter:toggleFilter\");\n return;\n });\n };\n\n /**\n * Fire the filterChanged event for the specified data.\n *\n * @param {object} data The data to include\n */\n var fireFilterChangedEvent = function(data) {\n M.util.js_pending(\"month-mini-filterChanged\");\n $('body').trigger(CalendarEvents.filterChanged, {\n type: data.eventtype,\n hidden: data.hidden,\n });\n M.util.js_complete(\"month-mini-filterChanged\");\n };\n\n /**\n * Get the filter data for the specified target.\n *\n * @param {jQuery} target The target node\n * @return {Object}\n */\n var getFilterData = function(target) {\n return {\n eventtype: target.data('eventtype'),\n hidden: target.data('eventtype-hidden'),\n };\n };\n\n return {\n init: function(root) {\n root = $(root);\n\n registerEventListeners(root);\n }\n };\n});\n"],"names":["define","$","CalendarSelectors","CalendarEvents","Str","Templates","toggleFilter","target","data","getFilterData","hidden","M","util","js_pending","get_string","eventtype","then","nameStr","name","icon","key","component","context","render","html","js","replaceNode","fireFilterChangedEvent","js_complete","trigger","filterChanged","type","init","root","on","eventFilterItem","e","currentTarget","preventDefault","viewUpdated","find","each","i","filter","registerEventListeners"],"mappings":";;;;;;;AAsBAA,uCAAO,CACH,SACA,0BACA,uBACA,WACA,mBAEJ,SACIC,EACAC,kBACAC,eACAC,IACAC,eAyBIC,aAAe,SAASC,YACpBC,KAAOC,cAAcF,eAGzBC,KAAKE,QAAUF,KAAKE,OAEpBC,EAAEC,KAAKC,WAAW,8CACXT,IAAIU,WAAW,YAAcN,KAAKO,UAAW,YACnDC,MAAK,SAASC,gBACXT,KAAKU,KAAOD,QACZT,KAAKW,MAAO,EACZX,KAAKY,IAAM,KAAOZ,KAAKO,UAAY,QACnCP,KAAKa,UAAY,OAEVb,QAEVQ,MAAK,SAASM,gBACJjB,UAAUkB,OAAO,iCAAkCD,YAE7DN,MAAK,SAASQ,KAAMC,WACVpB,UAAUqB,YAAYnB,OAAQiB,KAAMC,OAE9CT,MAAK,WACFW,uBAAuBnB,MACvBG,EAAEC,KAAKgB,YAAY,kDAUvBD,uBAAyB,SAASnB,MAClCG,EAAEC,KAAKC,WAAW,4BAClBZ,EAAE,QAAQ4B,QAAQ1B,eAAe2B,cAAe,CAC5CC,KAAMvB,KAAKO,UACXL,OAAQF,KAAKE,SAEjBC,EAAEC,KAAKgB,YAAY,6BASnBnB,cAAgB,SAASF,cAClB,CACHQ,UAAWR,OAAOC,KAAK,aACvBE,OAAQH,OAAOC,KAAK,4BAIrB,CACHwB,KAAM,SAASC,OA/EU,SAASA,MAClCA,KAAKC,GAAG,QAAShC,kBAAkBiC,iBAAiB,SAASC,OACrD7B,OAASN,EAAEmC,EAAEC,eAEjB/B,aAAaC,QAEb6B,EAAEE,oBAGNrC,EAAE,QAAQiC,GAAG/B,eAAeoC,aAAa,WACvBN,KAAKO,KAAKtC,kBAAkBiC,iBAElCM,MAAK,SAASC,EAAGC,YACrBA,OAAS1C,EAAE0C,SACAnC,KAAK,oBAAqB,KAC7BA,KAAOC,cAAckC,QACzBhB,uBAAuBnB,aAkE/BoC,CAFAX,KAAOhC,EAAEgC"}PK*\F?ݢamd/build/events.min.jsnu[define("core_calendar/events",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={created:"calendar-events:created",deleted:"calendar-events:deleted",deleteAll:"calendar-events:delete_all",updated:"calendar-events:updated",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",courseChanged:"calendar-events:course_changed",viewUpdated:"calendar-events:view_updated"},_exports.default})); //# sourceMappingURL=events.min.js.mapPK*\Кi33amd/build/selectors.min.jsnu[/** * CSS selectors for the calendar. * * @module core_calendar/selectors * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("core_calendar/selectors",[],(function(){return{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]",other:"[data-eventtype-other]"},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]",other:"[data-popover-eventtype-other]"},calendarPeriods:{month:"[data-period='month']"},courseSelector:'select[name="course"]',viewSelector:'div[data-region="view-selector"]',actions:{create:'[data-action="new-event-button"]',edit:'[data-action="edit"]',remove:'[data-action="delete"]',viewEvent:'[data-action="view-event"]',deleteSubscription:'[data-action="delete-subscription"]'},elements:{courseSelector:'select[name="course"]',dateContainer:".clickable.hasevent",dateContent:'[data-region="day-content"]',monthDetailed:".calendarmonth.calendartable"},today:".today",day:'[data-region="day"]',calendarMain:'[data-region="calendar"]',wrapper:".calendarwrapper",eventItem:'[data-type="event"]',links:{navLink:".calendarwrapper .arrow_link",eventLink:"[data-region='event-item']",miniDayLink:"[data-region='mini-day-link']",dayLink:"[data-action='view-day-link']"},containers:{loadingIcon:'[data-region="overlay-icon-container"]'},mainCalendar:".maincalendar .heightcontainer",fullCalendarView:"page-calendar-view",pageHeaderHeadings:".page-header-headings h1"}})); //# sourceMappingURL=selectors.min.js.mapPK*\3c +amd/build/month_navigation_drag_drop.min.jsnu[/** * A javascript module to handle calendar drag and drop in the calendar * month view navigation. * * This code is run each time the calendar month view is re-rendered. We * only register the event handlers once per page load so that the in place * DOM updates that happen on month change don't continue to register handlers. * * @module core_calendar/month_navigation_drag_drop * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("core_calendar/month_navigation_drag_drop",["jquery","core_calendar/drag_drop_data_store"],(function($,DataStore){var SELECTORS_DRAGGABLE='[draggable="true"][data-region="event-item"]',SELECTORS_DROP_ZONE='[data-drop-zone="nav-link"]',registered=!1,hoverTimer=null,root=null,updateHoverState=function(target,hovered){hovered?target.addClass("bg-primary text-white"):target.removeClass("bg-primary text-white")},addDropZoneIndicator=function(){root.find(SELECTORS_DROP_ZONE).addClass("drop-target")},removeDropZoneIndicator=function(){root.find(SELECTORS_DROP_ZONE).removeClass("drop-target")},getTargetFromEvent=function(e){var target=$(e.target).closest(SELECTORS_DROP_ZONE);return target.length?target:null},dragstartHandler=function(e){$(e.target).closest(SELECTORS_DRAGGABLE).length&&addDropZoneIndicator()},dragoverHandler=function(e){if(DataStore.hasEventId()){e.preventDefault();var target=getTargetFromEvent(e);target&&DataStore.hasEventId()&&(hoverTimer||(hoverTimer=setTimeout((function(){target.click(),hoverTimer=null}),1e3)),updateHoverState(target,!0),removeDropZoneIndicator())}},dragleaveHandler=function(e){if(DataStore.hasEventId()){var target=getTargetFromEvent(e);target&&(hoverTimer&&(clearTimeout(hoverTimer),hoverTimer=null),updateHoverState(target,!1),addDropZoneIndicator(),e.preventDefault())}},dropHandler=function(e){if(DataStore.hasEventId()){removeDropZoneIndicator();var target=getTargetFromEvent(e);target&&(updateHoverState(target,!1),e.preventDefault())}};return{init:function(rootElement){registered||(document.addEventListener("dragstart",dragstartHandler,!1),document.addEventListener("dragover",dragoverHandler,!1),document.addEventListener("dragleave",dragleaveHandler,!1),document.addEventListener("drop",dropHandler,!1),document.addEventListener("dragend",removeDropZoneIndicator,!1),registered=!0),root=$(rootElement),DataStore.hasEventId()&&addDropZoneIndicator()}}})); //# sourceMappingURL=month_navigation_drag_drop.min.js.mapPK*\C%amd/build/drag_drop_data_store.min.jsnu[/** * A javascript module to store calendar drag and drop data. * * This module is unfortunately required because of the limitations * of the HTML5 drag and drop API and it's ability to provide data * between the different stages of the drag/drop lifecycle. * * @module core_calendar/drag_drop_data_store * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("core_calendar/drag_drop_data_store",[],(function(){var eventId=null,durationDays=null,minTimestart=null,maxTimestart=null,minError=null,maxError=null,setEventId=function(id){eventId=id},setDurationDays=function(days){durationDays=days},setMinTimestart=function(timestamp){minTimestart=timestamp},setMaxTimestart=function(timestamp){maxTimestart=timestamp},setMinError=function(message){minError=message},setMaxError=function(message){maxError=message};return{setEventId:setEventId,getEventId:function(){return eventId},hasEventId:function(){return null!==eventId},setDurationDays:setDurationDays,getDurationDays:function(){return durationDays},setMinTimestart:setMinTimestart,getMinTimestart:function(){return minTimestart},hasMinTimestart:function(){return null!==minTimestart},setMaxTimestart:setMaxTimestart,getMaxTimestart:function(){return maxTimestart},hasMaxTimestart:function(){return null!==maxTimestart},setMinError:setMinError,getMinError:function(){return minError},setMaxError:setMaxError,getMaxError:function(){return maxError},clearAll:function(){setEventId(null),setDurationDays(null),setMinTimestart(null),setMaxTimestart(null),setMinError(null),setMaxError(null)}}})); //# sourceMappingURL=drag_drop_data_store.min.js.mapPK*\Ҡ33amd/build/crud.min.js.mapnu[{"version":3,"file":"crud.min.js","sources":["../src/crud.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A module to handle CRUD operations within the UI.\n *\n * @module core_calendar/crud\n * @copyright 2017 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/str',\n 'core/notification',\n 'core/modal_events',\n 'core_calendar/modal_event_form',\n 'core_calendar/repository',\n 'core_calendar/events',\n 'core_calendar/modal_delete',\n 'core_calendar/selectors',\n 'core/pending',\n 'core/modal_save_cancel',\n 'core/config',\n],\nfunction(\n $,\n Str,\n Notification,\n ModalEvents,\n ModalEventForm,\n CalendarRepository,\n CalendarEvents,\n CalendarModalDelete,\n CalendarSelectors,\n Pending,\n ModalSaveCancel,\n Config,\n) {\n\n /**\n * Prepares the action for the summary modal's delete action.\n *\n * @param {Number} eventId The ID of the event.\n * @param {string} eventTitle The event title.\n * @param {Number} eventCount The number of events in the series.\n * @return {Promise}\n */\n function confirmDeletion(eventId, eventTitle, eventCount) {\n var pendingPromise = new Pending('core_calendar/crud:confirmDeletion');\n var deleteStrings = [\n {\n key: 'deleteevent',\n component: 'calendar'\n },\n ];\n\n eventCount = parseInt(eventCount, 10);\n var deletePromise;\n var isRepeatedEvent = eventCount > 1;\n if (isRepeatedEvent) {\n deleteStrings.push({\n key: 'confirmeventseriesdelete',\n component: 'calendar',\n param: {\n name: eventTitle,\n count: eventCount,\n },\n });\n\n deletePromise = CalendarModalDelete.create();\n } else {\n deleteStrings.push({\n key: 'confirmeventdelete',\n component: 'calendar',\n param: eventTitle\n });\n\n\n deletePromise = ModalSaveCancel.create();\n }\n\n var stringsPromise = Str.get_strings(deleteStrings);\n\n var finalPromise = $.when(stringsPromise, deletePromise)\n .then(function(strings, deleteModal) {\n deleteModal.setRemoveOnClose(true);\n deleteModal.setTitle(strings[0]);\n deleteModal.setBody(strings[1]);\n if (!isRepeatedEvent) {\n deleteModal.setSaveButtonText(strings[0]);\n }\n\n deleteModal.show();\n\n deleteModal.getRoot().on(ModalEvents.save, function() {\n var pendingPromise = new Pending('calendar/crud:initModal:deletedevent');\n CalendarRepository.deleteEvent(eventId, false)\n .then(function() {\n $('body').trigger(CalendarEvents.deleted, [eventId, false]);\n return;\n })\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n });\n\n deleteModal.getRoot().on(CalendarEvents.deleteAll, function() {\n var pendingPromise = new Pending('calendar/crud:initModal:deletedallevent');\n CalendarRepository.deleteEvent(eventId, true)\n .then(function() {\n $('body').trigger(CalendarEvents.deleted, [eventId, true]);\n return;\n })\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n });\n\n return deleteModal;\n })\n .then(function(modal) {\n pendingPromise.resolve();\n\n return modal;\n })\n .catch(Notification.exception);\n\n return finalPromise;\n }\n\n /**\n * Create the event form modal for creating new events and\n * editing existing events.\n *\n * @method registerEventFormModal\n * @param {object} root The calendar root element\n * @return {object} The create modal promise\n */\n var registerEventFormModal = function(root) {\n var eventFormPromise = ModalEventForm.create();\n\n // Bind click event on the new event button.\n root.on('click', CalendarSelectors.actions.create, function(e) {\n eventFormPromise.then(function(modal) {\n var wrapper = root.find(CalendarSelectors.wrapper);\n\n var categoryId = wrapper.data('categoryid');\n const courseId = wrapper.data('courseid');\n if (typeof categoryId !== 'undefined' && courseId != Config.siteId) {\n modal.setCategoryId(categoryId);\n }\n\n // Attempt to find the cell for today.\n // If it can't be found, then use the start time of the first day on the calendar.\n var today = root.find(CalendarSelectors.today);\n var firstDay = root.find(CalendarSelectors.day);\n if (!today.length && firstDay.length) {\n modal.setStartTime(firstDay.data('newEventTimestamp'));\n }\n\n modal.setContextId(wrapper.data('contextId'));\n modal.setCourseId(wrapper.data('courseid'));\n modal.show();\n return;\n })\n .catch(Notification.exception);\n\n e.preventDefault();\n });\n\n root.on('click', CalendarSelectors.actions.edit, function(e) {\n e.preventDefault();\n var target = $(e.currentTarget),\n calendarWrapper = target.closest(CalendarSelectors.wrapper),\n eventWrapper = target.closest(CalendarSelectors.eventItem);\n\n eventFormPromise.then(function(modal) {\n // When something within the calendar tells us the user wants\n // to edit an event then show the event form modal.\n modal.setEventId(eventWrapper.data('eventId'));\n\n modal.setContextId(calendarWrapper.data('contextId'));\n modal.setCourseId(eventWrapper.data('courseId'));\n modal.show();\n\n e.stopImmediatePropagation();\n return;\n }).catch(Notification.exception);\n });\n\n\n return eventFormPromise;\n };\n /**\n * Register the listeners required to remove the event.\n *\n * @param {jQuery} root\n */\n function registerRemove(root) {\n root.on('click', CalendarSelectors.actions.remove, function(e) {\n // Fetch the event title, count, and pass them into the new dialogue.\n var eventSource = $(this).closest(CalendarSelectors.eventItem);\n var eventId = eventSource.data('eventId'),\n eventTitle = eventSource.data('eventTitle'),\n eventCount = eventSource.data('eventCount');\n confirmDeletion(eventId, eventTitle, eventCount);\n\n e.preventDefault();\n });\n }\n\n /**\n * Register the listeners required to edit the event.\n *\n * @param {jQuery} root\n * @param {Promise} eventFormModalPromise\n * @returns {Promise}\n */\n function registerEditListeners(root, eventFormModalPromise) {\n var pendingPromise = new Pending('core_calendar/crud:registerEditListeners');\n\n return eventFormModalPromise\n .then(function(modal) {\n // When something within the calendar tells us the user wants\n // to edit an event then show the event form modal.\n $('body').on(CalendarEvents.editEvent, function(e, eventId) {\n var target = root.find(`[data-event-id=${eventId}]`),\n calendarWrapper = root.find(CalendarSelectors.wrapper);\n\n modal.setEventId(eventId);\n modal.setContextId(calendarWrapper.data('contextId'));\n modal.setReturnElement(target);\n modal.show();\n\n e.stopImmediatePropagation();\n });\n return modal;\n })\n .then(function(modal) {\n pendingPromise.resolve();\n\n return modal;\n })\n .catch(Notification.exception);\n }\n\n return {\n registerRemove: registerRemove,\n registerEditListeners: registerEditListeners,\n registerEventFormModal: registerEventFormModal\n };\n});\n"],"names":["define","$","Str","Notification","ModalEvents","ModalEventForm","CalendarRepository","CalendarEvents","CalendarModalDelete","CalendarSelectors","Pending","ModalSaveCancel","Config","registerRemove","root","on","actions","remove","e","eventSource","this","closest","eventItem","eventId","eventTitle","eventCount","deletePromise","pendingPromise","deleteStrings","key","component","isRepeatedEvent","parseInt","push","param","name","count","create","stringsPromise","get_strings","when","then","strings","deleteModal","setRemoveOnClose","setTitle","setBody","setSaveButtonText","show","getRoot","save","deleteEvent","trigger","deleted","resolve","catch","exception","deleteAll","modal","confirmDeletion","data","preventDefault","registerEditListeners","eventFormModalPromise","editEvent","target","find","calendarWrapper","wrapper","setEventId","setContextId","setReturnElement","stopImmediatePropagation","registerEventFormModal","eventFormPromise","categoryId","courseId","siteId","setCategoryId","today","firstDay","day","length","setStartTime","setCourseId","edit","currentTarget","eventWrapper"],"mappings":";;;;;;;AAsBAA,4BAAO,CACH,SACA,WACA,oBACA,oBACA,iCACA,2BACA,uBACA,6BACA,0BACA,eACA,yBACA,gBAEJ,SACIC,EACAC,IACAC,aACAC,YACAC,eACAC,mBACAC,eACAC,oBACAC,kBACAC,QACAC,gBACAC,cAgNO,CACHC,wBAjDoBC,MACpBA,KAAKC,GAAG,QAASN,kBAAkBO,QAAQC,QAAQ,SAASC,OAEpDC,YAAclB,EAAEmB,MAAMC,QAAQZ,kBAAkBa,qBAxJnCC,QAASC,WAAYC,gBAUtCC,cATAC,eAAiB,IAAIjB,QAAQ,sCAC7BkB,cAAgB,CAChB,CACIC,IAAK,cACLC,UAAW,aAMfC,iBAFJN,WAAaO,SAASP,WAAY,KAEC,EAC/BM,iBACAH,cAAcK,KAAK,CACfJ,IAAK,2BACLC,UAAW,WACXI,MAAO,CACHC,KAAMX,WACNY,MAAOX,cAIfC,cAAgBlB,oBAAoB6B,WAEpCT,cAAcK,KAAK,CACfJ,IAAK,qBACLC,UAAW,WACXI,MAAOV,aAIXE,cAAgBf,gBAAgB0B,cAGhCC,eAAiBpC,IAAIqC,YAAYX,eAElB3B,EAAEuC,KAAKF,eAAgBZ,eACzCe,MAAK,SAASC,QAASC,oBACpBA,YAAYC,kBAAiB,GAC7BD,YAAYE,SAASH,QAAQ,IAC7BC,YAAYG,QAAQJ,QAAQ,IACvBX,iBACDY,YAAYI,kBAAkBL,QAAQ,IAG1CC,YAAYK,OAEZL,YAAYM,UAAUlC,GAAGX,YAAY8C,MAAM,eACnCvB,eAAiB,IAAIjB,QAAQ,wCACjCJ,mBAAmB6C,YAAY5B,SAAS,GACnCkB,MAAK,WACFxC,EAAE,QAAQmD,QAAQ7C,eAAe8C,QAAS,CAAC9B,SAAS,OAGvDkB,KAAKd,eAAe2B,SACpBC,MAAMpD,aAAaqD,cAG5Bb,YAAYM,UAAUlC,GAAGR,eAAekD,WAAW,eAC3C9B,eAAiB,IAAIjB,QAAQ,2CACjCJ,mBAAmB6C,YAAY5B,SAAS,GACnCkB,MAAK,WACFxC,EAAE,QAAQmD,QAAQ7C,eAAe8C,QAAS,CAAC9B,SAAS,OAGvDkB,KAAKd,eAAe2B,SACpBC,MAAMpD,aAAaqD,cAGrBb,eAEVF,MAAK,SAASiB,cACX/B,eAAe2B,UAERI,SAEVH,MAAMpD,aAAaqD,WAgFhBG,CAHcxC,YAAYyC,KAAK,WACdzC,YAAYyC,KAAK,cACjBzC,YAAYyC,KAAK,eAGlC1C,EAAE2C,qBAyCNC,+BA9B2BhD,KAAMiD,2BAC7BpC,eAAiB,IAAIjB,QAAQ,mDAE1BqD,sBACNtB,MAAK,SAASiB,cAGXzD,EAAE,QAAQc,GAAGR,eAAeyD,WAAW,SAAS9C,EAAGK,aAC3C0C,OAASnD,KAAKoD,8BAAuB3C,cACrC4C,gBAAkBrD,KAAKoD,KAAKzD,kBAAkB2D,SAElDV,MAAMW,WAAW9C,SACjBmC,MAAMY,aAAaH,gBAAgBP,KAAK,cACxCF,MAAMa,iBAAiBN,QACvBP,MAAMV,OAEN9B,EAAEsD,8BAECd,SAEVjB,MAAK,SAASiB,cACX/B,eAAe2B,UAERI,SAEVH,MAAMpD,aAAaqD,YAMpBiB,uBA/GyB,SAAS3D,UAC9B4D,iBAAmBrE,eAAegC,gBAGtCvB,KAAKC,GAAG,QAASN,kBAAkBO,QAAQqB,QAAQ,SAASnB,GACxDwD,iBAAiBjC,MAAK,SAASiB,WACvBU,QAAUtD,KAAKoD,KAAKzD,kBAAkB2D,SAEtCO,WAAaP,QAAQR,KAAK,oBACxBgB,SAAWR,QAAQR,KAAK,iBACJ,IAAfe,YAA8BC,UAAYhE,OAAOiE,QACxDnB,MAAMoB,cAAcH,gBAKpBI,MAAQjE,KAAKoD,KAAKzD,kBAAkBsE,OACpCC,SAAWlE,KAAKoD,KAAKzD,kBAAkBwE,MACtCF,MAAMG,QAAUF,SAASE,QAC1BxB,MAAMyB,aAAaH,SAASpB,KAAK,sBAGrCF,MAAMY,aAAaF,QAAQR,KAAK,cAChCF,MAAM0B,YAAYhB,QAAQR,KAAK,aAC/BF,MAAMV,UAGTO,MAAMpD,aAAaqD,WAEpBtC,EAAE2C,oBAGN/C,KAAKC,GAAG,QAASN,kBAAkBO,QAAQqE,MAAM,SAASnE,GACtDA,EAAE2C,qBACEI,OAAShE,EAAEiB,EAAEoE,eACbnB,gBAAkBF,OAAO5C,QAAQZ,kBAAkB2D,SACnDmB,aAAetB,OAAO5C,QAAQZ,kBAAkBa,WAEpDoD,iBAAiBjC,MAAK,SAASiB,OAG3BA,MAAMW,WAAWkB,aAAa3B,KAAK,YAEnCF,MAAMY,aAAaH,gBAAgBP,KAAK,cACxCF,MAAM0B,YAAYG,aAAa3B,KAAK,aACpCF,MAAMV,OAEN9B,EAAEsD,8BAEHjB,MAAMpD,aAAaqD,cAInBkB"}PK*\“ɵ!amd/build/view_manager.min.js.mapnu[{"version":3,"file":"view_manager.min.js","sources":["../src/view_manager.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A javascript module to handler calendar view changes.\n *\n * @module core_calendar/view_manager\n * @copyright 2017 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport * as CalendarRepository from 'core_calendar/repository';\nimport CalendarEvents from 'core_calendar/events';\nimport * as CalendarSelectors from 'core_calendar/selectors';\nimport ModalEvents from 'core/modal_events';\nimport SummaryModal from 'core_calendar/summary_modal';\nimport CustomEvents from 'core/custom_interaction_events';\nimport {getString} from 'core/str';\nimport Pending from 'core/pending';\nimport {prefetchStrings} from 'core/prefetch';\nimport Url from 'core/url';\nimport Config from 'core/config';\n\n/**\n * Limit number of events per day\n *\n */\nconst LIMIT_DAY_EVENTS = 5;\n\n/**\n * Hide day events if more than 5.\n *\n */\nexport const foldDayEvents = () => {\n const root = $(CalendarSelectors.elements.monthDetailed);\n const days = root.find(CalendarSelectors.day);\n if (days.length === 0) {\n return;\n }\n days.each(function() {\n const dayContainer = $(this);\n const eventsSelector = `${CalendarSelectors.elements.dateContent} ul li[data-event-eventtype]`;\n const filteredEventsSelector = `${CalendarSelectors.elements.dateContent} ul li[data-event-filtered=\"true\"]`;\n const moreEventsSelector = `${CalendarSelectors.elements.dateContent} [data-action=\"view-more-events\"]`;\n const events = dayContainer.find(eventsSelector);\n if (events.length === 0) {\n return;\n }\n\n const filteredEvents = dayContainer.find(filteredEventsSelector);\n const numberOfFiltered = filteredEvents.length;\n const numberOfEvents = events.length - numberOfFiltered;\n\n let count = 1;\n events.each(function() {\n const event = $(this);\n const isNotFiltered = event.attr('data-event-filtered') !== 'true';\n const offset = (numberOfEvents === LIMIT_DAY_EVENTS) ? 0 : 1;\n if (isNotFiltered) {\n if (count > LIMIT_DAY_EVENTS - offset) {\n event.attr('data-event-folded', 'true');\n event.hide();\n } else {\n event.attr('data-event-folded', 'false');\n event.show();\n count++;\n }\n } else {\n // It's being filtered out.\n event.attr('data-event-folded', 'false');\n }\n });\n\n const moreEventsLink = dayContainer.find(moreEventsSelector);\n if (numberOfEvents > LIMIT_DAY_EVENTS) {\n const numberOfHiddenEvents = numberOfEvents - LIMIT_DAY_EVENTS + 1;\n moreEventsLink.show();\n getString('moreevents', 'calendar', numberOfHiddenEvents).then(str => {\n const link = moreEventsLink.find('strong a');\n moreEventsLink.attr('data-event-folded', 'false');\n link.text(str);\n return str;\n }).catch(Notification.exception);\n } else {\n moreEventsLink.hide();\n }\n });\n};\n\n/**\n * Register and handle month calendar events.\n *\n * @param {string} pendingId pending id.\n */\nexport const registerEventListenersForMonthDetailed = (pendingId) => {\n const events = `${CalendarEvents.viewUpdated}`;\n $('body').on(events, function(e) {\n foldDayEvents(e);\n });\n foldDayEvents();\n $('body').on(CalendarEvents.filterChanged, function(e, data) {\n const root = $(CalendarSelectors.elements.monthDetailed);\n const pending = new Pending(pendingId);\n const target = root.find(CalendarSelectors.eventType[data.type]);\n const transitionPromise = $.Deferred();\n if (data.hidden) {\n transitionPromise.then(function() {\n target.attr('data-event-filtered', 'true');\n return target.hide().promise();\n }).fail();\n } else {\n transitionPromise.then(function() {\n target.attr('data-event-filtered', 'false');\n return target.show().promise();\n }).fail();\n }\n\n transitionPromise.then(function() {\n foldDayEvents();\n return;\n })\n .always(pending.resolve)\n .fail();\n\n transitionPromise.resolve();\n });\n};\n\n/**\n * Register event listeners for the module.\n *\n * @param {object} root The root element.\n * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.\n */\nconst registerEventListeners = (root, isCalendarBlock) => {\n root = $(root);\n\n // Bind click events to event links.\n root.on('click', CalendarSelectors.links.eventLink, (e) => {\n const target = e.target;\n let eventLink = null;\n let eventId = null;\n const pendingPromise = new Pending('core_calendar/view_manager:eventLink:click');\n\n if (target.matches(CalendarSelectors.actions.viewEvent)) {\n eventLink = target;\n } else {\n eventLink = target.closest(CalendarSelectors.actions.viewEvent);\n }\n\n if (eventLink) {\n eventId = eventLink.dataset.eventId;\n } else {\n eventId = target.querySelector(CalendarSelectors.actions.viewEvent).dataset.eventId;\n }\n\n if (eventId) {\n // A link was found. Show the modal.\n\n e.preventDefault();\n // We've handled the event so stop it from bubbling\n // and causing the day click handler to fire.\n e.stopPropagation();\n\n renderEventSummaryModal(eventId)\n .then(pendingPromise.resolve)\n .catch();\n } else {\n pendingPromise.resolve();\n }\n });\n\n root.on('click', CalendarSelectors.links.navLink, (e) => {\n const wrapper = root.find(CalendarSelectors.wrapper);\n const view = wrapper.data('view');\n const courseId = wrapper.data('courseid');\n const categoryId = wrapper.data('categoryid');\n const link = e.currentTarget;\n\n if (view === 'month' || view === 'monthblock') {\n changeMonth(root, link.href, link.dataset.year, link.dataset.month,\n courseId, categoryId, link.dataset.day, isCalendarBlock);\n e.preventDefault();\n } else if (view === 'day') {\n changeDay(root, link.href, link.dataset.year, link.dataset.month, link.dataset.day,\n courseId, categoryId, isCalendarBlock);\n e.preventDefault();\n }\n });\n\n const viewSelector = root.find(CalendarSelectors.viewSelector);\n CustomEvents.define(viewSelector, [CustomEvents.events.activate]);\n viewSelector.on(\n CustomEvents.events.activate,\n (e) => {\n e.preventDefault();\n\n const option = e.target;\n if (option.classList.contains('active')) {\n return;\n }\n\n const view = option.dataset.view,\n year = option.dataset.year,\n month = option.dataset.month,\n day = option.dataset.day,\n courseId = option.dataset.courseid,\n categoryId = option.dataset.categoryid;\n\n if (view == 'month') {\n refreshMonthContent(root, year, month, courseId, categoryId, root, 'core_calendar/calendar_month', day)\n .then(() => {\n updateUrl('?view=month&course=' + courseId);\n return;\n }).fail(Notification.exception);\n } else if (view == 'day') {\n refreshDayContent(root, year, month, day, courseId, categoryId, root, 'core_calendar/calendar_day')\n .then(() => {\n updateUrl('?view=day&course=' + courseId);\n return;\n }).fail(Notification.exception);\n } else if (view == 'upcoming') {\n reloadCurrentUpcoming(root, courseId, categoryId, root, 'core_calendar/calendar_upcoming')\n .then(() => {\n updateUrl('?view=upcoming&course=' + courseId);\n return;\n }).fail(Notification.exception);\n }\n }\n );\n};\n\n/**\n * Refresh the month content.\n *\n * @param {object} root The root element.\n * @param {number} year Year\n * @param {number} month Month\n * @param {number} courseId The id of the course whose events are shown\n * @param {number} categoryId The id of the category whose events are shown\n * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.\n * @param {string} template The template to be rendered.\n * @param {number} day Day (optional)\n * @return {promise}\n */\nexport const refreshMonthContent = (root, year, month, courseId, categoryId, target = null, template = '', day = 1) => {\n startLoading(root);\n\n target = target || root.find(CalendarSelectors.wrapper);\n template = template || root.attr('data-template');\n M.util.js_pending([root.get('id'), year, month, courseId].join('-'));\n const includenavigation = root.data('includenavigation');\n const mini = root.data('mini');\n const viewMode = target.data('view');\n return CalendarRepository.getCalendarMonthData(year, month, courseId, categoryId, includenavigation, mini, day, viewMode)\n .then(context => {\n return Templates.render(template, context);\n })\n .then((html, js) => {\n return Templates.replaceNode(target, html, js);\n })\n .then(() => {\n document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));\n return;\n })\n .always(() => {\n M.util.js_complete([root.get('id'), year, month, courseId].join('-'));\n return stopLoading(root);\n })\n .fail(Notification.exception);\n};\n\n/**\n * Handle changes to the current calendar view.\n *\n * @param {object} root The container element\n * @param {string} url The calendar url to be shown\n * @param {number} year Year\n * @param {number} month Month\n * @param {number} courseId The id of the course whose events are shown\n * @param {number} categoryId The id of the category whose events are shown\n * @param {number} day Day (optional)\n * @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block.\n * @return {promise}\n */\nexport const changeMonth = (root, url, year, month, courseId, categoryId, day = 1, isCalendarBlock = false) => {\n return refreshMonthContent(root, year, month, courseId, categoryId, null, '', day)\n .then((...args) => {\n if (url.length && url !== '#' && !isCalendarBlock) {\n updateUrl(url);\n }\n return args;\n })\n .then((...args) => {\n $('body').trigger(CalendarEvents.monthChanged, [year, month, courseId, categoryId, day, isCalendarBlock]);\n return args;\n });\n};\n\n/**\n * Reload the current month view data.\n *\n * @param {object} root The container element.\n * @param {number} courseId The course id.\n * @param {number} categoryId The id of the category whose events are shown\n * @return {promise}\n */\nexport const reloadCurrentMonth = (root, courseId = 0, categoryId = 0) => {\n const year = root.find(CalendarSelectors.wrapper).data('year');\n const month = root.find(CalendarSelectors.wrapper).data('month');\n const day = root.find(CalendarSelectors.wrapper).data('day');\n\n courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid');\n categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid');\n\n return refreshMonthContent(root, year, month, courseId, categoryId, null, '', day).\n then((...args) => {\n $('body').trigger(CalendarEvents.courseChanged, [year, month, courseId, categoryId]);\n return args;\n });\n};\n\n\n/**\n * Refresh the day content.\n *\n * @param {object} root The root element.\n * @param {number} year Year\n * @param {number} month Month\n * @param {number} day Day\n * @param {number} courseId The id of the course whose events are shown\n * @param {number} categoryId The id of the category whose events are shown\n * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.\n * @param {string} template The template to be rendered.\n * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.\n *\n * @return {promise}\n */\nexport const refreshDayContent = (root, year, month, day, courseId, categoryId,\n target = null, template = '', isCalendarBlock = false) => {\n startLoading(root);\n\n if (!target || target.length == 0){\n target = root.find(CalendarSelectors.wrapper);\n }\n template = template || root.attr('data-template');\n M.util.js_pending([root.get('id'), year, month, day, courseId, categoryId].join('-'));\n const includenavigation = root.data('includenavigation');\n return CalendarRepository.getCalendarDayData(year, month, day, courseId, categoryId, includenavigation)\n .then((context) => {\n context.viewingday = true;\n context.showviewselector = true;\n context.iscalendarblock = isCalendarBlock;\n return Templates.render(template, context);\n })\n .then((html, js) => {\n return Templates.replaceNode(target, html, js);\n })\n .then(() => {\n document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));\n return;\n })\n .always(() => {\n M.util.js_complete([root.get('id'), year, month, day, courseId, categoryId].join('-'));\n return stopLoading(root);\n })\n .fail(Notification.exception);\n};\n\n/**\n * Reload the current day view data.\n *\n * @param {object} root The container element.\n * @param {number} courseId The course id.\n * @param {number} categoryId The id of the category whose events are shown\n * @return {promise}\n */\nexport const reloadCurrentDay = (root, courseId = 0, categoryId = 0) => {\n const wrapper = root.find(CalendarSelectors.wrapper);\n const year = wrapper.data('year');\n const month = wrapper.data('month');\n const day = wrapper.data('day');\n\n courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid');\n categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid');\n\n return refreshDayContent(root, year, month, day, courseId, categoryId);\n};\n\n/**\n * Handle changes to the current calendar view.\n *\n * @param {object} root The root element.\n * @param {String} url The calendar url to be shown\n * @param {Number} year Year\n * @param {Number} month Month\n * @param {Number} day Day\n * @param {Number} courseId The id of the course whose events are shown\n * @param {Number} categoryId The id of the category whose events are shown\n * @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block.\n * @return {promise}\n */\nexport const changeDay = (root, url, year, month, day, courseId, categoryId, isCalendarBlock = false) => {\n return refreshDayContent(root, year, month, day, courseId, categoryId, null, '', isCalendarBlock)\n .then((...args) => {\n if (url.length && url !== '#' && !isCalendarBlock) {\n updateUrl(url);\n }\n return args;\n })\n .then((...args) => {\n $('body').trigger(CalendarEvents.dayChanged, [year, month, courseId, categoryId, isCalendarBlock]);\n return args;\n });\n};\n\n/**\n * Update calendar URL.\n *\n * @param {String} url The calendar url to be updated.\n */\nexport const updateUrl = (url) => {\n const viewingFullCalendar = document.getElementById(CalendarSelectors.fullCalendarView);\n\n // We want to update the url only if the user is viewing the full calendar.\n if (viewingFullCalendar) {\n window.history.pushState({}, '', url);\n }\n};\n\n/**\n * Set the element state to loading.\n *\n * @param {object} root The container element\n * @method startLoading\n */\nconst startLoading = (root) => {\n const loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon);\n\n loadingIconContainer.removeClass('hidden');\n};\n\n/**\n * Remove the loading state from the element.\n *\n * @param {object} root The container element\n * @method stopLoading\n */\nconst stopLoading = (root) => {\n const loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon);\n\n loadingIconContainer.addClass('hidden');\n};\n\n/**\n * Reload the current month view data.\n *\n * @param {object} root The container element.\n * @param {number} courseId The course id.\n * @param {number} categoryId The id of the category whose events are shown\n * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.\n * @param {string} template The template to be rendered.\n * @return {promise}\n */\nexport const reloadCurrentUpcoming = (root, courseId = 0, categoryId = 0, target = null, template = '') => {\n startLoading(root);\n\n target = target || root.find(CalendarSelectors.wrapper);\n template = template || root.attr('data-template');\n courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid');\n categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid');\n\n return CalendarRepository.getCalendarUpcomingData(courseId, categoryId)\n .then((context) => {\n context.viewingupcoming = true;\n context.showviewselector = true;\n return Templates.render(template, context);\n })\n .then((html, js) => {\n return Templates.replaceNode(target, html, js);\n })\n .then(() => {\n document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));\n return;\n })\n .always(function() {\n return stopLoading(root);\n })\n .fail(Notification.exception);\n};\n\n/**\n * Get the CSS class to apply for the given event type.\n *\n * @param {string} eventType The calendar event type\n * @return {string}\n */\nconst getEventTypeClassFromType = (eventType) => {\n return 'calendar_event_' + eventType;\n};\n\n/**\n * Render the event summary modal.\n *\n * @param {Number} eventId The calendar event id.\n * @returns {Promise}\n */\nconst renderEventSummaryModal = (eventId) => {\n const pendingPromise = new Pending('core_calendar/view_manager:renderEventSummaryModal');\n\n // Calendar repository promise.\n return CalendarRepository.getEventById(eventId)\n .then((getEventResponse) => {\n if (!getEventResponse.event) {\n throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);\n }\n\n return getEventResponse.event;\n })\n .then(eventData => {\n // Build the modal parameters from the event data.\n const modalParams = {\n title: eventData.name,\n body: Templates.render('core_calendar/event_summary_body', eventData),\n templateContext: {\n canedit: eventData.canedit,\n candelete: eventData.candelete,\n headerclasses: getEventTypeClassFromType(eventData.normalisedeventtype),\n isactionevent: eventData.isactionevent,\n url: eventData.url,\n action: eventData.action\n }\n };\n\n // Create the modal.\n return SummaryModal.create(modalParams);\n })\n .then(modal => {\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n });\n\n // Finally, render the modal!\n modal.show();\n\n return modal;\n })\n .then(modal => {\n pendingPromise.resolve();\n\n return modal;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Initializes the calendar component by prefetching strings, folding day events,\n * and registering event listeners.\n *\n * @param {HTMLElement} root - The root element where the calendar view manager and event listeners will be attached.\n * @param {string} view - A flag indicating whether this is a calendar block.\n * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.\n */\nexport const init = (root, view, isCalendarBlock) => {\n prefetchStrings('calendar', ['moreevents']);\n foldDayEvents();\n registerEventListeners(root, isCalendarBlock);\n const calendarTable = root.find(CalendarSelectors.elements.monthDetailed);\n if (calendarTable.length) {\n const pendingId = `month-detailed-${calendarTable.id}-filterChanged`;\n registerEventListenersForMonthDetailed(calendarTable, pendingId);\n }\n};\n\n/**\n * Handles the change of course and updates the relevant elements on the page.\n *\n * @param {integer} courseId - The ID of the new course.\n * @param {string} courseName - The name of the new course.\n * @returns {Promise} - A promise that resolves after the updates are applied.\n */\nexport const handleCourseChange = async(courseId, courseName) => {\n // Select the
        element inside the data-region=\"view-selector\".\n const ulElement = document.querySelector(CalendarSelectors.viewSelector + ' ul');\n // Select all
      • elements within the
          .\n const liElements = ulElement.querySelectorAll('li a');\n // Loop through the selected elements and update the courseid.\n liElements.forEach(element => {\n element.setAttribute('data-courseid', courseId);\n });\n\n const calendar = await getString('calendar', 'calendar');\n const pageHeaderHeadingsElement = document.querySelector(CalendarSelectors.pageHeaderHeadings);\n const courseUrl = Url.relativeUrl('/course/view.php', {id: courseId});\n // Apply the page header text.\n if (courseId !== Config.siteId) {\n pageHeaderHeadingsElement.innerHTML = calendar + ': ' + courseName + '';\n } else {\n pageHeaderHeadingsElement.innerHTML = calendar;\n }\n};"],"names":["foldDayEvents","days","CalendarSelectors","elements","monthDetailed","find","day","length","each","dayContainer","this","eventsSelector","dateContent","filteredEventsSelector","moreEventsSelector","events","numberOfFiltered","numberOfEvents","count","event","attr","hide","show","moreEventsLink","numberOfHiddenEvents","then","str","link","text","catch","Notification","exception","registerEventListenersForMonthDetailed","pendingId","CalendarEvents","viewUpdated","on","e","filterChanged","data","root","pending","Pending","target","eventType","type","transitionPromise","$","Deferred","hidden","promise","fail","always","resolve","refreshMonthContent","year","month","courseId","categoryId","template","startLoading","wrapper","M","util","js_pending","get","join","includenavigation","mini","viewMode","CalendarRepository","getCalendarMonthData","context","Templates","render","html","js","replaceNode","document","querySelector","dispatchEvent","CustomEvent","js_complete","stopLoading","changeMonth","url","isCalendarBlock","updateUrl","args","trigger","monthChanged","courseChanged","refreshDayContent","getCalendarDayData","viewingday","showviewselector","iscalendarblock","changeDay","dayChanged","getElementById","fullCalendarView","window","history","pushState","containers","loadingIcon","removeClass","addClass","reloadCurrentUpcoming","getCalendarUpcomingData","viewingupcoming","renderEventSummaryModal","eventId","pendingPromise","getEventById","getEventResponse","Error","eventData","modalParams","title","name","body","templateContext","canedit","candelete","headerclasses","normalisedeventtype","isactionevent","action","SummaryModal","create","modal","getRoot","ModalEvents","destroy","view","links","eventLink","matches","actions","viewEvent","closest","dataset","preventDefault","stopPropagation","navLink","currentTarget","href","viewSelector","define","CustomEvents","activate","option","classList","contains","courseid","categoryid","registerEventListeners","calendarTable","id","async","courseName","querySelectorAll","forEach","element","setAttribute","calendar","pageHeaderHeadingsElement","pageHeaderHeadings","courseUrl","Url","relativeUrl","Config","siteId","innerHTML"],"mappings":";;;;;;;69BAgDaA,cAAgB,WAEnBC,MADO,mBAAEC,kBAAkBC,SAASC,eACxBC,KAAKH,kBAAkBI,KACrB,IAAhBL,KAAKM,QAGTN,KAAKO,MAAK,iBACAC,cAAe,mBAAEC,MACjBC,yBAAoBT,kBAAkBC,SAASS,4CAC/CC,iCAA4BX,kBAAkBC,SAASS,kDACvDE,6BAAwBZ,kBAAkBC,SAASS,iDACnDG,OAASN,aAAaJ,KAAKM,mBACX,IAAlBI,OAAOR,oBAKLS,iBADiBP,aAAaJ,KAAKQ,wBACDN,OAClCU,eAAiBF,OAAOR,OAASS,qBAEnCE,MAAQ,EACZH,OAAOP,MAAK,iBACFW,OAAQ,mBAAET,MAC4C,SAAtCS,MAAMC,KAAK,uBAGzBF,MAhCK,GAAA,IA8BGD,eAAuC,EAAI,IAGnDE,MAAMC,KAAK,oBAAqB,QAChCD,MAAME,SAENF,MAAMC,KAAK,oBAAqB,SAChCD,MAAMG,OACNJ,SAIJC,MAAMC,KAAK,oBAAqB,kBAIlCG,eAAiBd,aAAaJ,KAAKS,uBACrCG,eA/Ca,EA+CsB,OAC7BO,qBAAuBP,eAhDhB,EAgDoD,EACjEM,eAAeD,0BACL,aAAc,WAAYE,sBAAsBC,MAAKC,YACrDC,KAAOJ,eAAelB,KAAK,mBACjCkB,eAAeH,KAAK,oBAAqB,SACzCO,KAAKC,KAAKF,KACHA,OACRG,MAAMC,sBAAaC,gBAEtBR,eAAeF,sDAUdW,uCAA0CC,kBAC7ClB,iBAAYmB,gBAAeC,iCAC/B,QAAQC,GAAGrB,QAAQ,SAASsB,GAC1BrC,mBAEJA,oCACE,QAAQoC,GAAGF,gBAAeI,eAAe,SAASD,EAAGE,YAC7CC,MAAO,mBAAEtC,kBAAkBC,SAASC,eACpCqC,QAAU,IAAIC,iBAAQT,WACtBU,OAASH,KAAKnC,KAAKH,kBAAkB0C,UAAUL,KAAKM,OACpDC,kBAAoBC,gBAAEC,WACxBT,KAAKU,OACLH,kBAAkBrB,MAAK,kBACnBkB,OAAOvB,KAAK,sBAAuB,QAC5BuB,OAAOtB,OAAO6B,aACtBC,OAEHL,kBAAkBrB,MAAK,kBACnBkB,OAAOvB,KAAK,sBAAuB,SAC5BuB,OAAOrB,OAAO4B,aACtBC,OAGPL,kBAAkBrB,MAAK,WACnBzB,mBAGHoD,OAAOX,QAAQY,SACfF,OAEDL,kBAAkBO,2GAyHbC,oBAAsB,SAACd,KAAMe,KAAMC,MAAOC,SAAUC,gBAAYf,8DAAS,KAAMgB,gEAAW,GAAIrD,2DAAM,EAC7GsD,aAAapB,MAEbG,OAASA,QAAUH,KAAKnC,KAAKH,kBAAkB2D,SAC/CF,SAAWA,UAAYnB,KAAKpB,KAAK,iBACjC0C,EAAEC,KAAKC,WAAW,CAACxB,KAAKyB,IAAI,MAAOV,KAAMC,MAAOC,UAAUS,KAAK,YACzDC,kBAAoB3B,KAAKD,KAAK,qBAC9B6B,KAAO5B,KAAKD,KAAK,QACjB8B,SAAW1B,OAAOJ,KAAK,eACtB+B,mBAAmBC,qBAAqBhB,KAAMC,MAAOC,SAAUC,WAAYS,kBAAmBC,KAAM9D,IAAK+D,UAC3G5C,MAAK+C,SACKC,mBAAUC,OAAOf,SAAUa,WAErC/C,MAAK,CAACkD,KAAMC,KACFH,mBAAUI,YAAYlC,OAAQgC,KAAMC,MAE9CnD,MAAK,KACFqD,SAASC,cAAc,QAAQC,cAAc,IAAIC,YAAY/C,gBAAeC,iBAG/EiB,QAAO,KACJU,EAAEC,KAAKmB,YAAY,CAAC1C,KAAKyB,IAAI,MAAOV,KAAMC,MAAOC,UAAUS,KAAK,MACzDiB,YAAY3C,SAEtBW,KAAKrB,sBAAaC,mEAgBdqD,YAAc,SAAC5C,KAAM6C,IAAK9B,KAAMC,MAAOC,SAAUC,gBAAYpD,2DAAM,EAAGgF,+EACxEhC,oBAAoBd,KAAMe,KAAMC,MAAOC,SAAUC,WAAY,KAAM,GAAIpD,KACzEmB,MAAK,WACE4D,IAAI9E,QAAkB,MAAR8E,MAAgBC,iBAC9BC,UAAUF,mCAFRG,6CAAAA,kCAICA,QAEV/D,MAAK,+BACA,QAAQgE,QAAQvD,gBAAewD,aAAc,CAACnC,KAAMC,MAAOC,SAAUC,WAAYpD,IAAKgF,iDADlFE,kDAAAA,oCAECA,sEAYe,SAAChD,UAAMiB,gEAAW,EAAGC,kEAAa,QAC1DH,KAAOf,KAAKnC,KAAKH,kBAAkB2D,SAAStB,KAAK,QACjDiB,MAAQhB,KAAKnC,KAAKH,kBAAkB2D,SAAStB,KAAK,SAClDjC,IAAMkC,KAAKnC,KAAKH,kBAAkB2D,SAAStB,KAAK,cAEtDkB,SAAWA,UAAYjB,KAAKnC,KAAKH,kBAAkB2D,SAAStB,KAAK,YACjEmB,WAAaA,YAAclB,KAAKnC,KAAKH,kBAAkB2D,SAAStB,KAAK,cAE9De,oBAAoBd,KAAMe,KAAMC,MAAOC,SAAUC,WAAY,KAAM,GAAIpD,KAC1EmB,MAAK,+BACC,QAAQgE,QAAQvD,gBAAeyD,cAAe,CAACpC,KAAMC,MAAOC,SAAUC,4CADnE8B,kDAAAA,oCAEEA,eAoBNI,kBAAoB,SAACpD,KAAMe,KAAMC,MAAOlD,IAAKmD,SAAUC,gBAChEf,8DAAS,KAAMgB,gEAAW,GAAI2B,wEAC9B1B,aAAapB,MAERG,QAA2B,GAAjBA,OAAOpC,SAClBoC,OAASH,KAAKnC,KAAKH,kBAAkB2D,UAEzCF,SAAWA,UAAYnB,KAAKpB,KAAK,iBACjC0C,EAAEC,KAAKC,WAAW,CAACxB,KAAKyB,IAAI,MAAOV,KAAMC,MAAOlD,IAAKmD,SAAUC,YAAYQ,KAAK,YAC1EC,kBAAoB3B,KAAKD,KAAK,4BAC7B+B,mBAAmBuB,mBAAmBtC,KAAMC,MAAOlD,IAAKmD,SAAUC,WAAYS,mBAChF1C,MAAM+C,UACHA,QAAQsB,YAAa,EACrBtB,QAAQuB,kBAAmB,EAC3BvB,QAAQwB,gBAAkBV,gBACnBb,mBAAUC,OAAOf,SAAUa,YAErC/C,MAAK,CAACkD,KAAMC,KACFH,mBAAUI,YAAYlC,OAAQgC,KAAMC,MAE9CnD,MAAK,KACFqD,SAASC,cAAc,QAAQC,cAAc,IAAIC,YAAY/C,gBAAeC,iBAG/EiB,QAAO,KACJU,EAAEC,KAAKmB,YAAY,CAAC1C,KAAKyB,IAAI,MAAOV,KAAMC,MAAOlD,IAAKmD,SAAUC,YAAYQ,KAAK,MAC1EiB,YAAY3C,SAEtBW,KAAKrB,sBAAaC,mFAWK,SAACS,UAAMiB,gEAAW,EAAGC,kEAAa,QACxDG,QAAUrB,KAAKnC,KAAKH,kBAAkB2D,SACtCN,KAAOM,QAAQtB,KAAK,QACpBiB,MAAQK,QAAQtB,KAAK,SACrBjC,IAAMuD,QAAQtB,KAAK,cAEzBkB,SAAWA,UAAYjB,KAAKnC,KAAKH,kBAAkB2D,SAAStB,KAAK,YACjEmB,WAAaA,YAAclB,KAAKnC,KAAKH,kBAAkB2D,SAAStB,KAAK,cAE9DqD,kBAAkBpD,KAAMe,KAAMC,MAAOlD,IAAKmD,SAAUC,mBAgBlDuC,UAAY,SAACzD,KAAM6C,IAAK9B,KAAMC,MAAOlD,IAAKmD,SAAUC,gBAAY4B,+EAClEM,kBAAkBpD,KAAMe,KAAMC,MAAOlD,IAAKmD,SAAUC,WAAY,KAAM,GAAI4B,iBAC5E7D,MAAK,WACE4D,IAAI9E,QAAkB,MAAR8E,MAAgBC,iBAC9BC,UAAUF,oCAFRG,kDAAAA,oCAICA,QAEV/D,MAAK,+BACA,QAAQgE,QAAQvD,gBAAegE,WAAY,CAAC3C,KAAMC,MAAOC,SAAUC,WAAY4B,iDAD3EE,kDAAAA,oCAECA,4CASND,UAAaF,MACMP,SAASqB,eAAejG,kBAAkBkG,mBAIlEC,OAAOC,QAAQC,UAAU,GAAI,GAAIlB,yCAUnCzB,aAAgBpB,OACWA,KAAKnC,KAAKH,kBAAkBsG,WAAWC,aAE/CC,YAAY,WAS/BvB,YAAe3C,OACYA,KAAKnC,KAAKH,kBAAkBsG,WAAWC,aAE/CE,SAAS,WAarBC,sBAAwB,SAACpE,UAAMiB,gEAAW,EAAGC,kEAAa,EAAGf,8DAAS,KAAMgB,gEAAW,UAChGC,aAAapB,MAEbG,OAASA,QAAUH,KAAKnC,KAAKH,kBAAkB2D,SAC/CF,SAAWA,UAAYnB,KAAKpB,KAAK,iBACjCqC,SAAWA,UAAYjB,KAAKnC,KAAKH,kBAAkB2D,SAAStB,KAAK,YACjEmB,WAAaA,YAAclB,KAAKnC,KAAKH,kBAAkB2D,SAAStB,KAAK,cAE9D+B,mBAAmBuC,wBAAwBpD,SAAUC,YACvDjC,MAAM+C,UACHA,QAAQsC,iBAAkB,EAC1BtC,QAAQuB,kBAAmB,EACpBtB,mBAAUC,OAAOf,SAAUa,YAErC/C,MAAK,CAACkD,KAAMC,KACFH,mBAAUI,YAAYlC,OAAQgC,KAAMC,MAE9CnD,MAAK,KACFqD,SAASC,cAAc,QAAQC,cAAc,IAAIC,YAAY/C,gBAAeC,iBAG/EiB,QAAO,kBACG+B,YAAY3C,SAEtBW,KAAKrB,sBAAaC,uEAmBrBgF,wBAA2BC,gBACvBC,eAAiB,IAAIvE,iBAAQ,6DAG5B4B,mBAAmB4C,aAAaF,SACtCvF,MAAM0F,uBACEA,iBAAiBhG,YACZ,IAAIiG,MAAM,mEAAqEJ,gBAGlFG,iBAAiBhG,SAE3BM,MAAK4F,kBAEIC,YAAc,CAChBC,MAAOF,UAAUG,KACjBC,KAAMhD,mBAAUC,OAAO,mCAAoC2C,WAC3DK,gBAAiB,CACbC,QAASN,UAAUM,QACnBC,UAAWP,UAAUO,UACrBC,eA9BmBjF,UA8BsByE,UAAUS,oBA7BxD,kBAAoBlF,WA8BfmF,cAAeV,UAAUU,cACzB1C,IAAKgC,UAAUhC,IACf2C,OAAQX,UAAUW,SAjCCpF,IAAAA,iBAsCpBqF,uBAAaC,OAAOZ,gBAE9B7F,MAAK0G,QAEFA,MAAMC,UAAUhG,GAAGiG,sBAAYpF,QAAQ,WAEnCkF,MAAMG,aAIVH,MAAM7G,OAEC6G,SAEV1G,MAAK0G,QACFlB,eAAe5D,UAER8E,SAEVtG,MAAMC,sBAAaC,0BAWJ,CAACS,KAAM+F,KAAMjD,iDACb,WAAY,CAAC,eAC7BtF,gBAjb2B,EAACwC,KAAM8C,oBAClC9C,MAAO,mBAAEA,OAGJJ,GAAG,QAASlC,kBAAkBsI,MAAMC,WAAYpG,UAC3CM,OAASN,EAAEM,WACb8F,UAAY,KACZzB,QAAU,WACRC,eAAiB,IAAIvE,iBAAQ,8CAG/B+F,UADA9F,OAAO+F,QAAQxI,kBAAkByI,QAAQC,WAC7BjG,OAEAA,OAAOkG,QAAQ3I,kBAAkByI,QAAQC,WAIrD5B,QADAyB,UACUA,UAAUK,QAAQ9B,QAElBrE,OAAOoC,cAAc7E,kBAAkByI,QAAQC,WAAWE,QAAQ9B,QAG5EA,SAGA3E,EAAE0G,iBAGF1G,EAAE2G,kBAEFjC,wBAAwBC,SACvBvF,KAAKwF,eAAe5D,SACpBxB,SAEDoF,eAAe5D,aAIvBb,KAAKJ,GAAG,QAASlC,kBAAkBsI,MAAMS,SAAU5G,UACzCwB,QAAUrB,KAAKnC,KAAKH,kBAAkB2D,SACtC0E,KAAO1E,QAAQtB,KAAK,QACpBkB,SAAWI,QAAQtB,KAAK,YACxBmB,WAAaG,QAAQtB,KAAK,cAC1BZ,KAAOU,EAAE6G,cAEF,UAATX,MAA6B,eAATA,MACpBnD,YAAY5C,KAAMb,KAAKwH,KAAMxH,KAAKmH,QAAQvF,KAAM5B,KAAKmH,QAAQtF,MACzDC,SAAUC,WAAY/B,KAAKmH,QAAQxI,IAAKgF,iBAC5CjD,EAAE0G,kBACc,QAATR,OACPtC,UAAUzD,KAAMb,KAAKwH,KAAMxH,KAAKmH,QAAQvF,KAAM5B,KAAKmH,QAAQtF,MAAO7B,KAAKmH,QAAQxI,IAC3EmD,SAAUC,WAAY4B,iBAC1BjD,EAAE0G,2BAIJK,aAAe5G,KAAKnC,KAAKH,kBAAkBkJ,iDACpCC,OAAOD,aAAc,CAACE,mCAAavI,OAAOwI,WACvDH,aAAahH,GACTkH,mCAAavI,OAAOwI,UACnBlH,IACGA,EAAE0G,uBAEIS,OAASnH,EAAEM,UACb6G,OAAOC,UAAUC,SAAS,uBAIxBnB,KAAOiB,OAAOV,QAAQP,KACxBhF,KAAOiG,OAAOV,QAAQvF,KACtBC,MAAQgG,OAAOV,QAAQtF,MACvBlD,IAAMkJ,OAAOV,QAAQxI,IACrBmD,SAAW+F,OAAOV,QAAQa,SAC1BjG,WAAa8F,OAAOV,QAAQc,WAEpB,SAARrB,KACAjF,oBAAoBd,KAAMe,KAAMC,MAAOC,SAAUC,WAAYlB,KAAM,+BAAgClC,KAC9FmB,MAAK,KACF8D,UAAU,sBAAwB9B,aAEnCN,KAAKrB,sBAAaC,WACV,OAARwG,KACP3C,kBAAkBpD,KAAMe,KAAMC,MAAOlD,IAAKmD,SAAUC,WAAYlB,KAAM,8BACjEf,MAAK,KACF8D,UAAU,oBAAsB9B,aAEjCN,KAAKrB,sBAAaC,WACV,YAARwG,MACP3B,sBAAsBpE,KAAMiB,SAAUC,WAAYlB,KAAM,mCACnDf,MAAK,KACF8D,UAAU,yBAA2B9B,aAEtCN,KAAKrB,sBAAaC,eAsVrC8H,CAAuBrH,KAAM8C,uBACvBwE,cAAgBtH,KAAKnC,KAAKH,kBAAkBC,SAASC,kBACvD0J,cAAcvJ,OAAQ,0BACcuJ,cAAcC,qBAClD/H,uCAAuC8H,6CAWbE,MAAMvG,SAAUwG,cAE5BnF,SAASC,cAAc7E,kBAAkBkJ,aAAe,OAE7Cc,iBAAiB,QAEnCC,SAAQC,UACfA,QAAQC,aAAa,gBAAiB5G,mBAGpC6G,eAAiB,kBAAU,WAAY,YACvCC,0BAA4BzF,SAASC,cAAc7E,kBAAkBsK,oBACrEC,UAAYC,aAAIC,YAAY,mBAAoB,CAACZ,GAAItG,WAEvDA,WAAamH,gBAAOC,OACpBN,0BAA0BO,UAAYR,SAAW,cAAgBG,UAAY,KAAOR,WAAa,OAEjGM,0BAA0BO,UAAYR"}PK*\$e&%amd/build/manage_subscriptions.min.jsnu[define("core_calendar/manage_subscriptions",["exports","core_calendar/selectors","core_calendar/repository","core/modal_save_cancel","core/modal_events","core/notification","core/prefetch","core/str","core/local/inplace_editable/events"],(function(_exports,CalendarSelectors,CalendarRepository,_modal_save_cancel,ModalEvents,_notification,_prefetch,_str,_events){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} /** * A module to handle Delete/Update operations of the manage subscription page. * * @module core_calendar/manage_subscriptions * @copyright 2021 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 4.0 */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,CalendarSelectors=_interopRequireWildcard(CalendarSelectors),CalendarRepository=_interopRequireWildcard(CalendarRepository),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),ModalEvents=_interopRequireWildcard(ModalEvents),_prefetch=_interopRequireDefault(_prefetch);const getSubscriptionName=element=>element.closest("tr").dataset.subname,registerEventListeners=()=>{document.addEventListener("click",(e=>{const deleteAction=e.target.closest(CalendarSelectors.actions.deleteSubscription);if(deleteAction){e.preventDefault();((element,messageCode)=>{const subscriptionName=getSubscriptionName(element);return _modal_save_cancel.default.create({title:(0,_str.getString)("confirmation","admin"),body:(0,_str.getString)(messageCode,"calendar",subscriptionName),buttons:{save:(0,_str.getString)("yes")}}).then((modal=>(modal.getRoot().on(ModalEvents.hidden,(()=>{element.focus()})),modal.show(),modal)))})(deleteAction,"confirmsubscriptiondelete").then((modal=>(modal.getRoot().on(ModalEvents.save,(()=>{const subscriptionId=parseInt(deleteAction.closest("tr").dataset.subid);CalendarRepository.deleteSubscription(subscriptionId).then((data=>{const response=(async(element,data)=>{const subscriptionName=getSubscriptionName(element),message=data.status?await(0,_str.getString)("subscriptionremoved","calendar",subscriptionName):data.warnings[0].message,type=data.status?"info":"error";return(0,_notification.addNotification)({message:message,type:type})})(deleteAction,data);return response.then((()=>{const subscriptionRow=(subscriptionId=>document.querySelector('tr[data-subid="'.concat(subscriptionId,'"]')))(subscriptionId);return subscriptionRow.remove()}))})).catch(_notification.exception)})),modal))).catch(_notification.exception)}})),document.addEventListener(_events.eventTypes.elementUpdated,(e=>{"core_calendar"==e.target.getAttribute("data-component")&&(0,_notification.fetchNotifications)()}))};_exports.init=()=>{_prefetch.default.prefetchStrings("moodle",["yes"]),_prefetch.default.prefetchStrings("core_admin",["confirmation"]),_prefetch.default.prefetchStrings("core_calendar",["confirmsubscriptiondelete","subscriptionremoved"]),registerEventListeners()}})); //# sourceMappingURL=manage_subscriptions.min.js.mapPK*\9 Aamd/build/calendar_mini.min.jsnu[/** * This module is the highest level module for the calendar. It is * responsible for initialising all of the components required for * the calendar to run. It also coordinates the interaction between * components by listening for and responding to different events * triggered within the calendar UI. * * @module core_calendar/calendar_mini * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("core_calendar/calendar_mini",["jquery","core_calendar/selectors","core_calendar/events","core_calendar/view_manager"],(function($,CalendarSelectors,CalendarEvents,CalendarViewManager){var reloadMonth=function(e){var root=e.data,body=$("body"),namespace="."+root.attr("id");root.is(":visible")?CalendarViewManager.reloadCurrentMonth(root):(body.off(CalendarEvents.created+namespace),body.off(CalendarEvents.deleted+namespace),body.off(CalendarEvents.updated+namespace),body.off(CalendarEvents.eventMoved+namespace))};return{init:function(root,loadOnInit){root=$(root),CalendarViewManager.init(root),function(root){$("body").on(CalendarEvents.filterChanged,(function(e,data){root.find(CalendarSelectors.eventType[data.type]).toggleClass("calendar_event_"+data.type,!data.hidden)}));var namespace="."+root.attr("id");$("body").on("change"+namespace,CalendarSelectors.elements.courseSelector,(function(){if(root.is(":visible")){var courseId=$(this).val();CalendarViewManager.reloadCurrentMonth(root,courseId,null)}else $("body").off("change"+namespace)}))}(root),function(root){var body=$("body"),namespace="."+root.attr("id");body.on(CalendarEvents.created+namespace,root,reloadMonth),body.on(CalendarEvents.deleted+namespace,root,reloadMonth),body.on(CalendarEvents.updated+namespace,root,reloadMonth),body.on(CalendarEvents.eventMoved+namespace,root,reloadMonth)}(root),loadOnInit&&CalendarViewManager.reloadCurrentMonth(root)}}})); //# sourceMappingURL=calendar_mini.min.js.mapPK*\QЈfFfFamd/build/calendar.min.js.mapnu[{"version":3,"file":"calendar.min.js","sources":["../src/calendar.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module is the highest level module for the calendar. It is\n * responsible for initialising all of the components required for\n * the calendar to run. It also coordinates the interaction between\n * components by listening for and responding to different events\n * triggered within the calendar UI.\n *\n * @module core_calendar/calendar\n * @copyright 2017 Simey Lameze \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/templates',\n 'core/notification',\n 'core_calendar/repository',\n 'core_calendar/events',\n 'core_calendar/view_manager',\n 'core_calendar/crud',\n 'core_calendar/selectors',\n 'core/url',\n 'core/str',\n],\nfunction(\n $,\n Templates,\n Notification,\n CalendarRepository,\n CalendarEvents,\n CalendarViewManager,\n CalendarCrud,\n CalendarSelectors,\n Url,\n Str,\n) {\n\n var SELECTORS = {\n ROOT: \"[data-region='calendar']\",\n DAY: \"[data-region='day']\",\n NEW_EVENT_BUTTON: \"[data-action='new-event-button']\",\n DAY_CONTENT: \"[data-region='day-content']\",\n LOADING_ICON: '.loading-icon',\n VIEW_DAY_LINK: \"[data-action='view-day-link']\",\n CALENDAR_MONTH_WRAPPER: \".calendarwrapper\",\n TODAY: '.today',\n DAY_NUMBER_CIRCLE: '.day-number-circle',\n DAY_NUMBER: '.day-number',\n SCREEN_READER_ANNOUNCEMENTS: '.calendar-announcements',\n CURRENT_MONTH: '.calendar-controls .current'\n };\n\n /**\n * Handler for the drag and drop move event. Provides a loading indicator\n * while the request is sent to the server to update the event start date.\n *\n * Triggers a eventMoved calendar javascript event if the event was successfully\n * updated.\n *\n * @param {event} e The calendar move event\n * @param {int} eventId The event id being moved\n * @param {object|null} originElement The jQuery element for where the event is moving from\n * @param {object} destinationElement The jQuery element for where the event is moving to\n */\n var handleMoveEvent = function(e, eventId, originElement, destinationElement) {\n var originTimestamp = null;\n var destinationTimestamp = destinationElement.attr('data-day-timestamp');\n\n if (originElement) {\n originTimestamp = originElement.attr('data-day-timestamp');\n }\n\n // If the event has actually changed day.\n if (!originElement || originTimestamp != destinationTimestamp) {\n Templates.render('core/loading', {})\n .then(function(html, js) {\n // First we show some loading icons in each of the days being affected.\n destinationElement.find(SELECTORS.DAY_CONTENT).addClass('hidden');\n Templates.appendNodeContents(destinationElement, html, js);\n\n if (originElement) {\n originElement.find(SELECTORS.DAY_CONTENT).addClass('hidden');\n Templates.appendNodeContents(originElement, html, js);\n }\n return;\n })\n .then(function() {\n // Send a request to the server to make the change.\n return CalendarRepository.updateEventStartDay(eventId, destinationTimestamp);\n })\n .then(function() {\n // If the update was successful then broadcast an event letting the calendar\n // know that an event has been moved.\n $('body').trigger(CalendarEvents.eventMoved, [eventId, originElement, destinationElement]);\n return;\n })\n .always(function() {\n // Always remove the loading icons regardless of whether the update\n // request was successful or not.\n var destinationLoadingElement = destinationElement.find(SELECTORS.LOADING_ICON);\n destinationElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden');\n Templates.replaceNode(destinationLoadingElement, '', '');\n\n if (originElement) {\n var originLoadingElement = originElement.find(SELECTORS.LOADING_ICON);\n originElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden');\n Templates.replaceNode(originLoadingElement, '', '');\n }\n return;\n })\n .catch(Notification.exception);\n }\n };\n\n /**\n * Listen to and handle any calendar events fired by the calendar UI.\n *\n * @method registerCalendarEventListeners\n * @param {object} root The calendar root element\n * @param {object} eventFormModalPromise A promise reolved with the event form modal\n */\n var registerCalendarEventListeners = function(root, eventFormModalPromise) {\n var body = $('body');\n\n body.on(CalendarEvents.created, function() {\n CalendarViewManager.reloadCurrentMonth(root);\n });\n body.on(CalendarEvents.deleted, function() {\n CalendarViewManager.reloadCurrentMonth(root);\n });\n body.on(CalendarEvents.updated, function() {\n CalendarViewManager.reloadCurrentMonth(root);\n });\n body.on(CalendarEvents.editActionEvent, function(e, url) {\n // Action events needs to be edit directly on the course module.\n window.location.assign(url);\n });\n // Handle the event fired by the drag and drop code.\n body.on(CalendarEvents.moveEvent, handleMoveEvent);\n // When an event is successfully moved we should updated the UI.\n body.on(CalendarEvents.eventMoved, function() {\n CalendarViewManager.reloadCurrentMonth(root);\n });\n // Announce the newly loaded month to screen readers.\n body.on(CalendarEvents.monthChanged, root, async function() {\n const monthName = body.find(SELECTORS.CURRENT_MONTH).text();\n const monthAnnoucement = await Str.get_string('newmonthannouncement', 'calendar', monthName);\n body.find(SELECTORS.SCREEN_READER_ANNOUNCEMENTS).html(monthAnnoucement);\n });\n\n CalendarCrud.registerEditListeners(root, eventFormModalPromise);\n };\n\n /**\n * Register event listeners for the module.\n *\n * @param {object} root The calendar root element\n * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.\n */\n var registerEventListeners = function(root, isCalendarBlock) {\n const viewingFullCalendar = document.getElementById(CalendarSelectors.fullCalendarView);\n // Listen the click on the day link to render the day view.\n root.on('click', SELECTORS.VIEW_DAY_LINK, function(e) {\n var dayLink = $(e.target).closest(SELECTORS.VIEW_DAY_LINK);\n var year = dayLink.data('year'),\n month = dayLink.data('month'),\n day = dayLink.data('day'),\n courseId = dayLink.data('courseid'),\n categoryId = dayLink.data('categoryid');\n const urlParams = {\n view: 'day',\n time: dayLink.data('timestamp'),\n course: courseId,\n };\n if (viewingFullCalendar) {\n // Construct the URL parameter string from the urlParams object.\n const urlParamString = Object.entries(urlParams)\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n .join('&');\n CalendarViewManager.refreshDayContent(root, year, month, day, courseId, categoryId, root,\n 'core_calendar/calendar_day', isCalendarBlock).then(function() {\n e.preventDefault();\n // Update the URL if it's not calendar block.\n if (!isCalendarBlock) {\n CalendarViewManager.updateUrl('?' + urlParamString);\n }\n return;\n }).catch(Notification.exception);\n } else {\n window.location.assign(Url.relativeUrl('calendar/view.php', urlParams));\n }\n });\n\n root.on('change', CalendarSelectors.elements.courseSelector, function() {\n var selectElement = $(this);\n var courseId = selectElement.val();\n const courseName = $(\"option:selected\", selectElement).text();\n CalendarViewManager.reloadCurrentMonth(root, courseId, null)\n .then(function() {\n // We need to get the selector again because the content has changed.\n return root.find(CalendarSelectors.elements.courseSelector).val(courseId);\n })\n .then(function() {\n CalendarViewManager.updateUrl('?view=month&course=' + courseId);\n CalendarViewManager.handleCourseChange(Number(courseId), courseName);\n return;\n })\n .catch(Notification.exception);\n });\n\n var eventFormPromise = CalendarCrud.registerEventFormModal(root),\n contextId = $(SELECTORS.CALENDAR_MONTH_WRAPPER).data('context-id');\n registerCalendarEventListeners(root, eventFormPromise);\n\n if (contextId) {\n // Bind click events to calendar days.\n root.on('click', SELECTORS.DAY, function(e) {\n var target = $(e.target);\n const displayingSmallBlockCalendar = root.parents('aside').data('blockregion') === 'side-pre';\n\n if (!viewingFullCalendar && displayingSmallBlockCalendar) {\n const dateContainer = target.closest(SELECTORS.DAY);\n const wrapper = target.closest(CalendarSelectors.wrapper);\n const courseId = wrapper.data('courseid');\n const params = {\n view: 'day',\n time: dateContainer.data('day-timestamp'),\n course: courseId,\n };\n window.location.assign(Url.relativeUrl('calendar/view.php', params));\n } else {\n const hasViewDayLink = target.closest(SELECTORS.VIEW_DAY_LINK).length;\n const shouldShowNewEventModal = !hasViewDayLink;\n if (shouldShowNewEventModal) {\n var startTime = $(this).attr('data-new-event-timestamp');\n eventFormPromise.then(function(modal) {\n var wrapper = target.closest(CalendarSelectors.wrapper);\n modal.setCourseId(wrapper.data('courseid'));\n\n var categoryId = wrapper.data('categoryid');\n if (typeof categoryId !== 'undefined') {\n modal.setCategoryId(categoryId);\n }\n\n modal.setContextId(wrapper.data('contextId'));\n modal.setStartTime(startTime);\n modal.show();\n return;\n }).catch(Notification.exception);\n }\n }\n e.preventDefault();\n });\n }\n };\n\n return {\n /**\n * Initializes the calendar view manager and registers event listeners.\n *\n * @param {HTMLElement} root - The root element where the calendar view manager and event listeners will be attached.\n * @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block.\n */\n init: function(root, isCalendarBlock = false) {\n root = $(root);\n CalendarViewManager.init(root, 'month', isCalendarBlock);\n registerEventListeners(root, isCalendarBlock);\n }\n };\n});\n"],"names":["define","$","Templates","Notification","CalendarRepository","CalendarEvents","CalendarViewManager","CalendarCrud","CalendarSelectors","Url","Str","SELECTORS","handleMoveEvent","e","eventId","originElement","destinationElement","originTimestamp","destinationTimestamp","attr","render","then","html","js","find","addClass","appendNodeContents","updateEventStartDay","trigger","eventMoved","always","destinationLoadingElement","removeClass","replaceNode","originLoadingElement","catch","exception","registerEventListeners","root","isCalendarBlock","viewingFullCalendar","document","getElementById","fullCalendarView","on","dayLink","target","closest","year","data","month","day","courseId","categoryId","urlParams","view","time","course","urlParamString","Object","entries","map","_ref","key","value","encodeURIComponent","join","refreshDayContent","preventDefault","updateUrl","window","location","assign","relativeUrl","elements","courseSelector","selectElement","this","val","courseName","text","reloadCurrentMonth","handleCourseChange","Number","eventFormPromise","registerEventFormModal","contextId","eventFormModalPromise","body","created","deleted","updated","editActionEvent","url","moveEvent","monthChanged","async","monthName","monthAnnoucement","get_string","registerEditListeners","registerCalendarEventListeners","displayingSmallBlockCalendar","parents","dateContainer","wrapper","params","length","startTime","modal","setCourseId","setCategoryId","setContextId","setStartTime","show","init"],"mappings":";;;;;;;;;;;AA0BAA,gCAAO,CACH,SACA,iBACA,oBACA,2BACA,uBACA,6BACA,qBACA,0BACA,WACA,aAEJ,SACIC,EACAC,UACAC,aACAC,mBACAC,eACAC,oBACAC,aACAC,kBACAC,IACAC,SAGIC,cAEK,sBAFLA,sBAIa,8BAJbA,uBAKc,gBALdA,wBAMe,gCANfA,iCAOwB,mBAPxBA,sCAW6B,0BAX7BA,wBAYe,8BAefC,gBAAkB,SAASC,EAAGC,QAASC,cAAeC,wBAClDC,gBAAkB,KAClBC,qBAAuBF,mBAAmBG,KAAK,sBAE/CJ,gBACAE,gBAAkBF,cAAcI,KAAK,uBAIpCJ,eAAiBE,iBAAmBC,sBACrChB,UAAUkB,OAAO,eAAgB,IAC5BC,MAAK,SAASC,KAAMC,IAEjBP,mBAAmBQ,KAAKb,uBAAuBc,SAAS,UACxDvB,UAAUwB,mBAAmBV,mBAAoBM,KAAMC,IAEnDR,gBACAA,cAAcS,KAAKb,uBAAuBc,SAAS,UACnDvB,UAAUwB,mBAAmBX,cAAeO,KAAMC,QAIzDF,MAAK,kBAEKjB,mBAAmBuB,oBAAoBb,QAASI,yBAE1DG,MAAK,WAGFpB,EAAE,QAAQ2B,QAAQvB,eAAewB,WAAY,CAACf,QAASC,cAAeC,wBAGzEc,QAAO,eAGAC,0BAA4Bf,mBAAmBQ,KAAKb,2BACxDK,mBAAmBQ,KAAKb,uBAAuBqB,YAAY,UAC3D9B,UAAU+B,YAAYF,0BAA2B,GAAI,IAEjDhB,cAAe,KACXmB,qBAAuBnB,cAAcS,KAAKb,wBAC9CI,cAAcS,KAAKb,uBAAuBqB,YAAY,UACtD9B,UAAU+B,YAAYC,qBAAsB,GAAI,QAIvDC,MAAMhC,aAAaiC,YAiD5BC,uBAAyB,SAASC,KAAMC,uBAClCC,oBAAsBC,SAASC,eAAelC,kBAAkBmC,kBAEtEL,KAAKM,GAAG,QAASjC,yBAAyB,SAASE,OAC3CgC,QAAU5C,EAAEY,EAAEiC,QAAQC,QAAQpC,yBAC9BqC,KAAOH,QAAQI,KAAK,QACpBC,MAAQL,QAAQI,KAAK,SACrBE,IAAMN,QAAQI,KAAK,OACnBG,SAAWP,QAAQI,KAAK,YACxBI,WAAaR,QAAQI,KAAK,oBACxBK,UAAY,CACdC,KAAM,MACNC,KAAMX,QAAQI,KAAK,aACnBQ,OAAQL,aAERZ,oBAAqB,OAEfkB,eAAiBC,OAAOC,QAAQN,WACjCO,KAAIC,WAAEC,IAAKC,4BAAcC,mBAAmBF,iBAAQE,mBAAmBD,WACvEE,KAAK,KACV5D,oBAAoB6D,kBAAkB7B,KAAMU,KAAME,MAAOC,IAAKC,SAAUC,WAAYf,KAChF,6BAA8BC,iBAAiBlB,MAAK,WACpDR,EAAEuD,iBAEG7B,iBACDjC,oBAAoB+D,UAAU,IAAMX,mBAGzCvB,MAAMhC,aAAaiC,gBAEtBkC,OAAOC,SAASC,OAAO/D,IAAIgE,YAAY,oBAAqBnB,eAIpEhB,KAAKM,GAAG,SAAUpC,kBAAkBkE,SAASC,gBAAgB,eACrDC,cAAgB3E,EAAE4E,MAClBzB,SAAWwB,cAAcE,YACvBC,WAAa9E,EAAE,kBAAmB2E,eAAeI,OACvD1E,oBAAoB2E,mBAAmB3C,KAAMc,SAAU,MAClD/B,MAAK,kBAEKiB,KAAKd,KAAKhB,kBAAkBkE,SAASC,gBAAgBG,IAAI1B,aAEnE/B,MAAK,WACFf,oBAAoB+D,UAAU,sBAAwBjB,UACtD9C,oBAAoB4E,mBAAmBC,OAAO/B,UAAW2B,eAG5D5C,MAAMhC,aAAaiC,kBAGxBgD,iBAAmB7E,aAAa8E,uBAAuB/C,MACvDgD,UAAYrF,EAAEU,kCAAkCsC,KAAK,eA1FxB,SAASX,KAAMiD,2BAC5CC,KAAOvF,EAAE,QAEbuF,KAAK5C,GAAGvC,eAAeoF,SAAS,WAC5BnF,oBAAoB2E,mBAAmB3C,SAE3CkD,KAAK5C,GAAGvC,eAAeqF,SAAS,WAC5BpF,oBAAoB2E,mBAAmB3C,SAE3CkD,KAAK5C,GAAGvC,eAAesF,SAAS,WAC5BrF,oBAAoB2E,mBAAmB3C,SAE3CkD,KAAK5C,GAAGvC,eAAeuF,iBAAiB,SAAS/E,EAAGgF,KAEhDvB,OAAOC,SAASC,OAAOqB,QAG3BL,KAAK5C,GAAGvC,eAAeyF,UAAWlF,iBAElC4E,KAAK5C,GAAGvC,eAAewB,YAAY,WAC/BvB,oBAAoB2E,mBAAmB3C,SAG3CkD,KAAK5C,GAAGvC,eAAe0F,aAAczD,MAAM0D,uBACjCC,UAAYT,KAAKhE,KAAKb,yBAAyBqE,OAC/CkB,uBAAyBxF,IAAIyF,WAAW,uBAAwB,WAAYF,WAClFT,KAAKhE,KAAKb,uCAAuCW,KAAK4E,qBAG1D3F,aAAa6F,sBAAsB9D,KAAMiD,uBA8DzCc,CAA+B/D,KAAM8C,kBAEjCE,WAEAhD,KAAKM,GAAG,QAASjC,eAAe,SAASE,OACjCiC,OAAS7C,EAAEY,EAAEiC,cACXwD,6BAA6E,aAA9ChE,KAAKiE,QAAQ,SAAStD,KAAK,mBAE3DT,qBAAuB8D,6BAA8B,OAChDE,cAAgB1D,OAAOC,QAAQpC,eAE/ByC,SADUN,OAAOC,QAAQvC,kBAAkBiG,SACxBxD,KAAK,YACxByD,OAAS,CACXnD,KAAM,MACNC,KAAMgD,cAAcvD,KAAK,iBACzBQ,OAAQL,UAEZkB,OAAOC,SAASC,OAAO/D,IAAIgE,YAAY,oBAAqBiC,aACzD,KACoB5D,OAAOC,QAAQpC,yBAAyBgG,OAElC,KACrBC,UAAY3G,EAAE4E,MAAM1D,KAAK,4BAC7BiE,iBAAiB/D,MAAK,SAASwF,WACvBJ,QAAU3D,OAAOC,QAAQvC,kBAAkBiG,SAC/CI,MAAMC,YAAYL,QAAQxD,KAAK,iBAE3BI,WAAaoD,QAAQxD,KAAK,mBACJ,IAAfI,YACPwD,MAAME,cAAc1D,YAGxBwD,MAAMG,aAAaP,QAAQxD,KAAK,cAChC4D,MAAMI,aAAaL,WACnBC,MAAMK,UAEP/E,MAAMhC,aAAaiC,YAG9BvB,EAAEuD,2BAKP,CAOH+C,KAAM,SAAS7E,UAAMC,wEACjBD,KAAOrC,EAAEqC,MACThC,oBAAoB6G,KAAK7E,KAAM,QAASC,iBACxCF,uBAAuBC,KAAMC"}PK*\~5amd/build/popover.min.js.mapnu[{"version":3,"file":"popover.min.js","sources":["../src/popover.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript popover for the `core_calendar` subsystem.\n *\n * @module core_calendar/popover\n * @copyright 2021 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.0\n */\n\nimport Popover from 'theme_boost/bootstrap/popover';\nimport * as CalendarSelectors from 'core_calendar/selectors';\n\n/**\n * Check if we are allowing to enable the popover or not.\n * @param {Element} dateContainer\n * @returns {boolean}\n */\nconst isPopoverAvailable = (dateContainer) => {\n return window.getComputedStyle(dateContainer.querySelector(CalendarSelectors.elements.dateContent)).display === 'none';\n};\n\nconst isPopoverConfigured = new Map();\nconst showPopover = target => {\n const dateContainer = target.closest(CalendarSelectors.elements.dateContainer);\n if (!isPopoverConfigured.has(dateContainer)) {\n const config = {\n trigger: 'manual',\n placement: 'top',\n html: true,\n title: dateContainer.dataset.title,\n content: () => {\n const source = dateContainer.querySelector(CalendarSelectors.elements.dateContent);\n return source ? source.querySelector('.hidden').innerHTML : '';\n },\n 'animation': false,\n };\n new Popover(target, config);\n\n isPopoverConfigured.set(dateContainer, true);\n }\n\n if (isPopoverAvailable(dateContainer)) {\n Popover.getInstance(target).show();\n target.addEventListener('mouseleave', hidePopover);\n target.addEventListener('focusout', hidePopover);\n // Set up the hide function to the click event type.\n target.addEventListener('click', hidePopover);\n }\n};\n\nconst hidePopover = e => {\n const target = e.target;\n const dateContainer = e.target.closest(CalendarSelectors.elements.dateContainer);\n if (!dateContainer) {\n return;\n }\n if (isPopoverConfigured.has(dateContainer)) {\n const isTargetActive = target.contains(document.activeElement);\n const isTargetHover = target.matches(':hover');\n\n // Checks if a target element is clicked or pressed.\n const isTargetClicked = document.activeElement.contains(target);\n\n let removeListener = true;\n if (!isTargetActive && !isTargetHover) {\n Popover.getOrCreateInstance(target).hide();\n } else if (isTargetClicked) {\n Popover.getOrCreateInstance(document.activeElement).hide();\n } else {\n removeListener = false;\n }\n\n if (removeListener) {\n target.removeEventListener('mouseleave', hidePopover);\n target.removeEventListener('focusout', hidePopover);\n target.removeEventListener('click', hidePopover);\n }\n }\n};\n\n/**\n * Register events for date container.\n */\nconst registerEventListeners = () => {\n const showPopoverHandler = (e) => {\n const dayLink = e.target.closest(CalendarSelectors.links.dayLink);\n if (!dayLink) {\n return;\n }\n\n e.preventDefault();\n showPopover(dayLink);\n };\n\n document.addEventListener('mouseover', showPopoverHandler);\n document.addEventListener('focusin', showPopoverHandler);\n};\n\nlet listenersRegistered = false;\nif (!listenersRegistered) {\n registerEventListeners();\n listenersRegistered = true;\n}\n"],"names":["isPopoverConfigured","Map","showPopover","target","dateContainer","closest","CalendarSelectors","elements","has","config","trigger","placement","html","title","dataset","content","source","querySelector","dateContent","innerHTML","Popover","set","window","getComputedStyle","display","isPopoverAvailable","getInstance","show","addEventListener","hidePopover","e","isTargetActive","contains","document","activeElement","isTargetHover","matches","isTargetClicked","removeListener","getOrCreateInstance","hide","removeEventListener","listenersRegistered","showPopoverHandler","dayLink","links","preventDefault","registerEventListeners"],"mappings":";;;;;;;;0gCAoCMA,oBAAsB,IAAIC,IAC1BC,YAAcC,eACVC,cAAgBD,OAAOE,QAAQC,kBAAkBC,SAASH,mBAC3DJ,oBAAoBQ,IAAIJ,eAAgB,OACnCK,OAAS,CACXC,QAAS,SACTC,UAAW,MACXC,MAAM,EACNC,MAAOT,cAAcU,QAAQD,MAC7BE,QAAS,WACCC,OAASZ,cAAca,cAAcX,kBAAkBC,SAASW,oBAC/DF,OAASA,OAAOC,cAAc,WAAWE,UAAY,eAEnD,OAEbC,iBAAQjB,OAAQM,QAEpBT,oBAAoBqB,IAAIjB,eAAe,GArBnBA,CAAAA,eACwF,SAAzGkB,OAAOC,iBAAiBnB,cAAca,cAAcX,kBAAkBC,SAASW,cAAcM,QAuBhGC,CAAmBrB,kCACXsB,YAAYvB,QAAQwB,OAC5BxB,OAAOyB,iBAAiB,aAAcC,aACtC1B,OAAOyB,iBAAiB,WAAYC,aAEpC1B,OAAOyB,iBAAiB,QAASC,eAInCA,YAAcC,UACV3B,OAAS2B,EAAE3B,OACXC,cAAgB0B,EAAE3B,OAAOE,QAAQC,kBAAkBC,SAASH,kBAC7DA,eAGDJ,oBAAoBQ,IAAIJ,eAAgB,OAClC2B,eAAiB5B,OAAO6B,SAASC,SAASC,eAC1CC,cAAgBhC,OAAOiC,QAAQ,UAG/BC,gBAAkBJ,SAASC,cAAcF,SAAS7B,YAEpDmC,gBAAiB,EAChBP,gBAAmBI,cAEbE,iCACCE,oBAAoBN,SAASC,eAAeM,OAEpDF,gBAAiB,mBAJTC,oBAAoBpC,QAAQqC,OAOpCF,iBACAnC,OAAOsC,oBAAoB,aAAcZ,aACzC1B,OAAOsC,oBAAoB,WAAYZ,aACvC1B,OAAOsC,oBAAoB,QAASZ,oBAuB5Ca,qBAAsB,EACrBA,sBAhB0B,YACrBC,mBAAsBb,UAClBc,QAAUd,EAAE3B,OAAOE,QAAQC,kBAAkBuC,MAAMD,SACpDA,UAILd,EAAEgB,iBACF5C,YAAY0C,WAGhBX,SAASL,iBAAiB,YAAae,oBACvCV,SAASL,iBAAiB,UAAWe,qBAKrCI,GACAL,qBAAsB"}PK*\$#E#E)amd/build/month_view_drag_drop.min.js.mapnu[{"version":3,"file":"month_view_drag_drop.min.js","sources":["../src/month_view_drag_drop.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A javascript module to handle calendar drag and drop in the calendar\n * month view.\n *\n * @module core_calendar/month_view_drag_drop\n * @copyright 2017 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/notification',\n 'core/str',\n 'core_calendar/events',\n 'core_calendar/drag_drop_data_store'\n ],\n function(\n $,\n Notification,\n Str,\n CalendarEvents,\n DataStore\n ) {\n\n var SELECTORS = {\n ROOT: \"[data-region='calendar']\",\n DRAGGABLE: '[draggable=\"true\"][data-region=\"event-item\"]',\n DROP_ZONE: '[data-drop-zone=\"month-view-day\"]',\n WEEK: '[data-region=\"month-view-week\"]',\n };\n var INVALID_DROP_ZONE_CLASS = 'bg-faded';\n var INVALID_HOVER_CLASS = 'bg-danger text-white';\n var VALID_HOVER_CLASS = 'bg-primary text-white';\n var ALL_CLASSES = INVALID_DROP_ZONE_CLASS + ' ' + INVALID_HOVER_CLASS + ' ' + VALID_HOVER_CLASS;\n /* @var {bool} registered If the event listeners have been added */\n var registered = false;\n\n /**\n * Get the correct drop zone element from the given javascript\n * event.\n *\n * @param {event} e The javascript event\n * @return {object|null}\n */\n var getDropZoneFromEvent = function(e) {\n var dropZone = $(e.target).closest(SELECTORS.DROP_ZONE);\n return (dropZone.length) ? dropZone : null;\n };\n\n /**\n * Determine if the given dropzone element is within the acceptable\n * time range.\n *\n * The drop zone timestamp is midnight on that day so we should check\n * that the event's acceptable timestart value\n *\n * @param {object} dropZone The drop zone day from the calendar\n * @return {bool}\n */\n var isValidDropZone = function(dropZone) {\n var dropTimestamp = dropZone.attr('data-day-timestamp');\n var minTimestart = DataStore.getMinTimestart();\n var maxTimestart = DataStore.getMaxTimestart();\n\n if (minTimestart && minTimestart > dropTimestamp) {\n return false;\n }\n\n if (maxTimestart && maxTimestart < dropTimestamp) {\n return false;\n }\n\n return true;\n };\n\n /**\n * Get the error string to display for a given drop zone element\n * if it is invalid.\n *\n * @param {object} dropZone The drop zone day from the calendar\n * @return {string}\n */\n var getDropZoneError = function(dropZone) {\n var dropTimestamp = dropZone.attr('data-day-timestamp');\n var minTimestart = DataStore.getMinTimestart();\n var maxTimestart = DataStore.getMaxTimestart();\n\n if (minTimestart && minTimestart > dropTimestamp) {\n return DataStore.getMinError();\n }\n\n if (maxTimestart && maxTimestart < dropTimestamp) {\n return DataStore.getMaxError();\n }\n\n return null;\n };\n\n /**\n * Remove all of the styling from each of the drop zones in the calendar.\n */\n var clearAllDropZonesState = function() {\n $(SELECTORS.ROOT).find(SELECTORS.DROP_ZONE).each(function(index, dropZone) {\n dropZone = $(dropZone);\n dropZone.removeClass(ALL_CLASSES);\n });\n };\n\n /**\n * Update the hover state for the event in the calendar to reflect\n * which days the event will be moved to.\n *\n * If the drop zone is not being hovered then it will apply some\n * styling to reflect whether the drop zone is a valid or invalid\n * drop place for the current dragging event.\n *\n * This funciton supports events spanning multiple days and will\n * recurse to highlight (or remove highlight) each of the days\n * that the event will be moved to.\n *\n * For example: An event with a duration of 3 days will have\n * 3 days highlighted when it's dragged elsewhere in the calendar.\n * The current drag target and the 2 days following it (including\n * wrapping to the next week if necessary).\n *\n * @param {string|object} dropZone The drag target element\n * @param {bool} hovered If the target is hovered or not\n * @param {Number} count How many days to highlight (default to duration)\n */\n var updateHoverState = function(dropZone, hovered, count) {\n if (typeof count === 'undefined') {\n // This is how many days we need to highlight.\n count = DataStore.getDurationDays();\n }\n\n var valid = isValidDropZone(dropZone);\n dropZone.removeClass(ALL_CLASSES);\n\n if (hovered) {\n\n if (valid) {\n dropZone.addClass(VALID_HOVER_CLASS);\n } else {\n dropZone.addClass(INVALID_HOVER_CLASS);\n }\n } else {\n dropZone.removeClass(VALID_HOVER_CLASS + ' ' + INVALID_HOVER_CLASS);\n\n if (!valid) {\n dropZone.addClass(INVALID_DROP_ZONE_CLASS);\n }\n }\n\n count--;\n\n // If we've still got days to highlight then we should\n // find the next day.\n if (count > 0) {\n var nextDropZone = dropZone.next();\n\n // If there are no more days in this week then we\n // need to move down to the next week in the calendar.\n if (!nextDropZone.length) {\n var nextWeek = dropZone.closest(SELECTORS.WEEK).next();\n\n if (nextWeek.length) {\n nextDropZone = nextWeek.children(SELECTORS.DROP_ZONE).first();\n }\n }\n\n // If we found another day then let's recursively\n // update it's hover state.\n if (nextDropZone.length) {\n updateHoverState(nextDropZone, hovered, count);\n }\n }\n };\n\n /**\n * Find all of the calendar event drop zones in the calendar and update the display\n * for the user to indicate which zones are valid and invalid.\n */\n var updateAllDropZonesState = function() {\n $(SELECTORS.ROOT).find(SELECTORS.DROP_ZONE).each(function(index, dropZone) {\n dropZone = $(dropZone);\n\n if (!isValidDropZone(dropZone)) {\n updateHoverState(dropZone, false);\n }\n });\n };\n\n\n /**\n * Set up the module level variables to track which event is being\n * dragged and how many days it spans.\n *\n * @param {event} e The dragstart event\n */\n var dragstartHandler = function(e) {\n var target = $(e.target);\n var draggableElement = target.closest(SELECTORS.DRAGGABLE);\n\n if (!draggableElement.length) {\n return;\n }\n\n var eventElement = draggableElement.find('[data-event-id]');\n var eventId = eventElement.attr('data-event-id');\n var minTimestart = draggableElement.attr('data-min-day-timestamp');\n var maxTimestart = draggableElement.attr('data-max-day-timestamp');\n var minError = draggableElement.attr('data-min-day-error');\n var maxError = draggableElement.attr('data-max-day-error');\n var eventsSelector = SELECTORS.ROOT + ' [data-event-id=\"' + eventId + '\"]';\n var duration = $(eventsSelector).length;\n\n DataStore.setEventId(eventId);\n DataStore.setDurationDays(duration);\n\n if (minTimestart) {\n DataStore.setMinTimestart(minTimestart);\n }\n\n if (maxTimestart) {\n DataStore.setMaxTimestart(maxTimestart);\n }\n\n if (minError) {\n DataStore.setMinError(minError);\n }\n\n if (maxError) {\n DataStore.setMaxError(maxError);\n }\n\n e.dataTransfer.effectAllowed = \"move\";\n e.dataTransfer.dropEffect = \"move\";\n // Firefox requires a value to be set here or the drag won't\n // work and the dragover handler won't fire.\n e.dataTransfer.setData('text/plain', eventId);\n e.dropEffect = \"move\";\n\n updateAllDropZonesState();\n };\n\n /**\n * Update the hover state of the target day element when\n * the user is dragging an event over it.\n *\n * This will add a visual indicator to the calendar UI to\n * indicate which day(s) the event will be moved to.\n *\n * @param {event} e The dragstart event\n */\n var dragoverHandler = function(e) {\n // Ignore dragging of non calendar events.\n if (!DataStore.hasEventId()) {\n return;\n }\n\n e.preventDefault();\n\n var dropZone = getDropZoneFromEvent(e);\n\n if (!dropZone) {\n return;\n }\n\n updateHoverState(dropZone, true);\n };\n\n /**\n * Update the hover state of the target day element that was\n * previously dragged over but has is no longer a drag target.\n *\n * This will remove the visual indicator from the calendar UI\n * that was added by the dragoverHandler.\n *\n * @param {event} e The dragstart event\n */\n var dragleaveHandler = function(e) {\n // Ignore dragging of non calendar events.\n if (!DataStore.hasEventId()) {\n return;\n }\n\n var dropZone = getDropZoneFromEvent(e);\n\n if (!dropZone) {\n return;\n }\n\n updateHoverState(dropZone, false);\n e.preventDefault();\n };\n\n /**\n * Determines the event element, origin day, and destination day\n * once the user drops the calendar event. These three bits of data\n * are provided as the payload to the \"moveEvent\" calendar javascript\n * event that is fired.\n *\n * This will remove the visual indicator from the calendar UI\n * that was added by the dragoverHandler.\n *\n * @param {event} e The dragstart event\n */\n var dropHandler = function(e) {\n // Ignore dragging of non calendar events.\n if (!DataStore.hasEventId()) {\n return;\n }\n\n var dropZone = getDropZoneFromEvent(e);\n\n if (!dropZone) {\n DataStore.clearAll();\n clearAllDropZonesState();\n return;\n }\n\n if (isValidDropZone(dropZone)) {\n var eventId = DataStore.getEventId();\n var eventElementSelector = SELECTORS.ROOT + ' [data-event-id=\"' + eventId + '\"]';\n var eventElement = $(eventElementSelector);\n var origin = null;\n\n if (eventElement.length) {\n origin = eventElement.closest(SELECTORS.DROP_ZONE);\n }\n\n $('body').trigger(CalendarEvents.moveEvent, [eventId, origin, dropZone]);\n } else {\n // If the drop zone is not valid then there is not need for us to\n // try to process it. Instead we can just show an error to the user.\n var message = getDropZoneError(dropZone);\n Str.get_string('errorinvaliddate', 'calendar').then(function(string) {\n Notification.exception({\n name: string,\n message: message || string\n });\n });\n }\n\n DataStore.clearAll();\n clearAllDropZonesState();\n\n e.preventDefault();\n };\n\n /**\n * Clear the data store and remove the drag indicators from the UI\n * when the drag event has finished.\n */\n var dragendHandler = function() {\n DataStore.clearAll();\n clearAllDropZonesState();\n };\n\n /**\n * Re-render the drop zones in the new month to highlight\n * which areas are or aren't acceptable to drop the calendar\n * event.\n */\n var calendarMonthChangedHandler = function() {\n updateAllDropZonesState();\n };\n\n return {\n /**\n * Initialise the event handlers for the drag events.\n */\n init: function() {\n if (!registered) {\n // These handlers are only added the first time the module\n // is loaded because we don't want to have a new listener\n // added each time the \"init\" function is called otherwise we'll\n // end up with lots of stale handlers.\n document.addEventListener('dragstart', dragstartHandler, false);\n document.addEventListener('dragover', dragoverHandler, false);\n document.addEventListener('dragleave', dragleaveHandler, false);\n document.addEventListener('drop', dropHandler, false);\n document.addEventListener('dragend', dragendHandler, false);\n $('body').on(CalendarEvents.monthChanged, calendarMonthChangedHandler);\n registered = true;\n }\n },\n };\n});\n"],"names":["define","$","Notification","Str","CalendarEvents","DataStore","SELECTORS","ALL_CLASSES","INVALID_DROP_ZONE_CLASS","registered","getDropZoneFromEvent","e","dropZone","target","closest","length","isValidDropZone","dropTimestamp","attr","minTimestart","getMinTimestart","maxTimestart","getMaxTimestart","clearAllDropZonesState","find","each","index","removeClass","updateHoverState","hovered","count","getDurationDays","valid","addClass","VALID_HOVER_CLASS","nextDropZone","next","nextWeek","children","first","updateAllDropZonesState","dragstartHandler","draggableElement","eventId","minError","maxError","duration","setEventId","setDurationDays","setMinTimestart","setMaxTimestart","setMinError","setMaxError","dataTransfer","effectAllowed","dropEffect","setData","dragoverHandler","hasEventId","preventDefault","dragleaveHandler","dropHandler","clearAll","getEventId","eventElement","origin","trigger","moveEvent","message","getMinError","getMaxError","getDropZoneError","get_string","then","string","exception","name","dragendHandler","calendarMonthChangedHandler","init","document","addEventListener","on","monthChanged"],"mappings":";;;;;;;;AAuBAA,4CAAO,CACK,SACA,oBACA,WACA,uBACA,uCAEJ,SACIC,EACAC,aACAC,IACAC,eACAC,eAGJC,eACM,2BADNA,oBAEW,+CAFXA,oBAGW,oCAHXA,eAIM,kCAKNC,YAAcC,sDAEdC,YAAa,EASbC,qBAAuB,SAASC,OAC5BC,SAAWX,EAAEU,EAAEE,QAAQC,QAAQR,4BAC3BM,SAASG,OAAUH,SAAW,MAatCI,gBAAkB,SAASJ,cACvBK,cAAgBL,SAASM,KAAK,sBAC9BC,aAAed,UAAUe,kBACzBC,aAAehB,UAAUiB,0BAEzBH,cAAgBA,aAAeF,kBAI/BI,cAAgBA,aAAeJ,gBAiCnCM,uBAAyB,WACzBtB,EAAEK,gBAAgBkB,KAAKlB,qBAAqBmB,MAAK,SAASC,MAAOd,WAC7DA,SAAWX,EAAEW,WACJe,YAAYpB,iBAyBzBqB,iBAAmB,SAAShB,SAAUiB,QAASC,YAC1B,IAAVA,QAEPA,MAAQzB,UAAU0B,uBAGlBC,MAAQhB,gBAAgBJ,aAC5BA,SAASe,YAAYpB,aAEjBsB,QAEIG,MACApB,SAASqB,SA7GG,yBA+GZrB,SAASqB,SAhHK,yBAmHlBrB,SAASe,YAAYO,8CAEhBF,OACDpB,SAASqB,SAvHS,eA2H1BH,MAIY,EAAG,KACPK,aAAevB,SAASwB,WAIvBD,aAAapB,OAAQ,KAClBsB,SAAWzB,SAASE,QAAQR,gBAAgB8B,OAE5CC,SAAStB,SACToB,aAAeE,SAASC,SAAShC,qBAAqBiC,SAM1DJ,aAAapB,QACba,iBAAiBO,aAAcN,QAASC,SAShDU,wBAA0B,WAC1BvC,EAAEK,gBAAgBkB,KAAKlB,qBAAqBmB,MAAK,SAASC,MAAOd,UAC7DA,SAAWX,EAAEW,UAERI,gBAAgBJ,WACjBgB,iBAAiBhB,UAAU,OAYnC6B,iBAAmB,SAAS9B,OAExB+B,iBADSzC,EAAEU,EAAEE,QACaC,QAAQR,wBAEjCoC,iBAAiB3B,YAKlB4B,QADeD,iBAAiBlB,KAAK,mBACdN,KAAK,iBAC5BC,aAAeuB,iBAAiBxB,KAAK,0BACrCG,aAAeqB,iBAAiBxB,KAAK,0BACrC0B,SAAWF,iBAAiBxB,KAAK,sBACjC2B,SAAWH,iBAAiBxB,KAAK,sBAEjC4B,SAAW7C,EADMK,eAAiB,oBAAsBqC,QAAU,MACrC5B,OAEjCV,UAAU0C,WAAWJ,SACrBtC,UAAU2C,gBAAgBF,UAEtB3B,cACAd,UAAU4C,gBAAgB9B,cAG1BE,cACAhB,UAAU6C,gBAAgB7B,cAG1BuB,UACAvC,UAAU8C,YAAYP,UAGtBC,UACAxC,UAAU+C,YAAYP,UAG1BlC,EAAE0C,aAAaC,cAAgB,OAC/B3C,EAAE0C,aAAaE,WAAa,OAG5B5C,EAAE0C,aAAaG,QAAQ,aAAcb,SACrChC,EAAE4C,WAAa,OAEff,4BAYAiB,gBAAkB,SAAS9C,MAEtBN,UAAUqD,cAIf/C,EAAEgD,qBAEE/C,SAAWF,qBAAqBC,GAE/BC,UAILgB,iBAAiBhB,UAAU,KAY3BgD,iBAAmB,SAASjD,MAEvBN,UAAUqD,kBAIX9C,SAAWF,qBAAqBC,GAE/BC,WAILgB,iBAAiBhB,UAAU,GAC3BD,EAAEgD,oBAcFE,YAAc,SAASlD,MAElBN,UAAUqD,kBAIX9C,SAAWF,qBAAqBC,OAE/BC,gBACDP,UAAUyD,gBACVvC,4BAIAP,gBAAgBJ,UAAW,KACvB+B,QAAUtC,UAAU0D,aAEpBC,aAAe/D,EADQK,eAAiB,oBAAsBqC,QAAU,MAExEsB,OAAS,KAETD,aAAajD,SACbkD,OAASD,aAAalD,QAAQR,sBAGlCL,EAAE,QAAQiE,QAAQ9D,eAAe+D,UAAW,CAACxB,QAASsB,OAAQrD,eAC3D,KAGCwD,QA7PW,SAASxD,cACxBK,cAAgBL,SAASM,KAAK,sBAC9BC,aAAed,UAAUe,kBACzBC,aAAehB,UAAUiB,yBAEzBH,cAAgBA,aAAeF,cACxBZ,UAAUgE,cAGjBhD,cAAgBA,aAAeJ,cACxBZ,UAAUiE,cAGd,KAgPWC,CAAiB3D,UAC/BT,IAAIqE,WAAW,mBAAoB,YAAYC,MAAK,SAASC,QACzDxE,aAAayE,UAAU,CACnBC,KAAMF,OACNN,QAASA,SAAWM,YAKhCrE,UAAUyD,WACVvC,yBAEAZ,EAAEgD,mBAOFkB,eAAiB,WACjBxE,UAAUyD,WACVvC,0BAQAuD,4BAA8B,WAC9BtC,iCAGG,CAIHuC,KAAM,WACGtE,aAKDuE,SAASC,iBAAiB,YAAaxC,kBAAkB,GACzDuC,SAASC,iBAAiB,WAAYxB,iBAAiB,GACvDuB,SAASC,iBAAiB,YAAarB,kBAAkB,GACzDoB,SAASC,iBAAiB,OAAQpB,aAAa,GAC/CmB,SAASC,iBAAiB,UAAWJ,gBAAgB,GACrD5E,EAAE,QAAQiF,GAAG9E,eAAe+E,aAAcL,6BAC1CrE,YAAa"}PK*\бamd/build/export.min.js.mapnu[{"version":3,"file":"export.min.js","sources":["../src/export.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A javascript module to enhance the calendar export form.\n *\n * @module core_calendar/export\n * @copyright 2021 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport 'core/copy_to_clipboard';\n\n/**\n * Selectors for the calendar export page.\n *\n * @property {string} copyUrlId The element ID of the Copy URL button.\n */\nconst selectors = {\n copyUrlId: 'copyexporturl',\n};\n\n/**\n * Initialises the calendar export JS module.\n *\n * @method init\n */\nexport const init = () => {\n // Enable the copy URL button and focus on it.\n const copyUrl = document.getElementById(selectors.copyUrlId);\n copyUrl.removeAttribute('disabled');\n copyUrl.focus();\n};\n"],"names":["selectors","copyUrl","document","getElementById","removeAttribute","focus"],"mappings":";;;;;;;;MA8BMA,oBACS,8BAQK,WAEVC,QAAUC,SAASC,eAAeH,qBACxCC,QAAQG,gBAAgB,YACxBH,QAAQI"}PK*\[  amd/build/modal_delete.min.jsnu[define("core_calendar/modal_delete",["exports","jquery","core/custom_interaction_events","core/modal","core/modal_events","./events"],(function(_exports,_jquery,CustomEvents,_modal,_modal_events,_events){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),CustomEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CustomEvents),_modal=_interopRequireDefault(_modal),_modal_events=_interopRequireDefault(_modal_events),_events=_interopRequireDefault(_events);const SELECTORS_DELETE_ONE_BUTTON='[data-action="deleteone"]',SELECTORS_DELETE_ALL_BUTTON='[data-action="deleteall"]',SELECTORS_CANCEL_BUTTON='[data-action="cancel"]';class ModalDelete extends _modal.default{constructor(root){super(root),this.setRemoveOnClose(!0)}registerEventListeners(){super.registerEventListeners(this),this.getModal().on(CustomEvents.events.activate,SELECTORS_DELETE_ONE_BUTTON,((e,data)=>{const saveEvent=_jquery.default.Event(_modal_events.default.save);this.getRoot().trigger(saveEvent,this),saveEvent.isDefaultPrevented()||(this.hide(),data.originalEvent.preventDefault())})),this.getModal().on(CustomEvents.events.activate,SELECTORS_DELETE_ALL_BUTTON,((e,data)=>{const saveEvent=_jquery.default.Event(_events.default.deleteAll);this.getRoot().trigger(saveEvent,this),saveEvent.isDefaultPrevented()||(this.hide(),data.originalEvent.preventDefault())})),this.getModal().on(CustomEvents.events.activate,SELECTORS_CANCEL_BUTTON,((e,data)=>{const cancelEvent=_jquery.default.Event(_modal_events.default.cancel);this.getRoot().trigger(cancelEvent,this),cancelEvent.isDefaultPrevented()||(this.hide(),data.originalEvent.preventDefault())}))}}return _exports.default=ModalDelete,_defineProperty(ModalDelete,"TYPE","core_calendar-modal_delete"),_defineProperty(ModalDelete,"TEMPLATE","calendar/event_delete_modal"),ModalDelete.registerModalType(),_exports.default})); //# sourceMappingURL=modal_delete.min.js.mapPK*\cnZNN amd/build/calendar_filter.min.jsnu[/** * This module is responsible for the calendar filter. * * @module core_calendar/calendar_filter * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("core_calendar/calendar_filter",["jquery","core_calendar/selectors","core_calendar/events","core/str","core/templates"],(function($,CalendarSelectors,CalendarEvents,Str,Templates){var toggleFilter=function(target){var data=getFilterData(target);return data.hidden=!data.hidden,M.util.js_pending("core_calendar/calendar_filter:toggleFilter"),Str.get_string("eventtype"+data.eventtype,"calendar").then((function(nameStr){return data.name=nameStr,data.icon=!0,data.key="i/"+data.eventtype+"event",data.component="core",data})).then((function(context){return Templates.render("core_calendar/event_filter_key",context)})).then((function(html,js){return Templates.replaceNode(target,html,js)})).then((function(){fireFilterChangedEvent(data),M.util.js_complete("core_calendar/calendar_filter:toggleFilter")}))},fireFilterChangedEvent=function(data){M.util.js_pending("month-mini-filterChanged"),$("body").trigger(CalendarEvents.filterChanged,{type:data.eventtype,hidden:data.hidden}),M.util.js_complete("month-mini-filterChanged")},getFilterData=function(target){return{eventtype:target.data("eventtype"),hidden:target.data("eventtype-hidden")}};return{init:function(root){!function(root){root.on("click",CalendarSelectors.eventFilterItem,(function(e){var target=$(e.currentTarget);toggleFilter(target),e.preventDefault()})),$("body").on(CalendarEvents.viewUpdated,(function(){root.find(CalendarSelectors.eventFilterItem).each((function(i,filter){if((filter=$(filter)).data("eventtype-hidden")){var data=getFilterData(filter);fireFilterChangedEvent(data)}}))}))}(root=$(root))}}})); //# sourceMappingURL=calendar_filter.min.js.mapPK*\0:EE"amd/build/calendar_mini.min.js.mapnu[{"version":3,"file":"calendar_mini.min.js","sources":["../src/calendar_mini.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module is the highest level module for the calendar. It is\n * responsible for initialising all of the components required for\n * the calendar to run. It also coordinates the interaction between\n * components by listening for and responding to different events\n * triggered within the calendar UI.\n *\n * @module core_calendar/calendar_mini\n * @copyright 2017 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core_calendar/selectors',\n 'core_calendar/events',\n 'core_calendar/view_manager',\n],\nfunction(\n $,\n CalendarSelectors,\n CalendarEvents,\n CalendarViewManager\n) {\n\n /**\n * Listen to and handle any calendar events fired by the calendar UI.\n *\n * @method registerCalendarEventListeners\n * @param {object} root The calendar root element\n */\n var registerCalendarEventListeners = function(root) {\n var body = $('body');\n var namespace = '.' + root.attr('id');\n\n body.on(CalendarEvents.created + namespace, root, reloadMonth);\n body.on(CalendarEvents.deleted + namespace, root, reloadMonth);\n body.on(CalendarEvents.updated + namespace, root, reloadMonth);\n body.on(CalendarEvents.eventMoved + namespace, root, reloadMonth);\n };\n\n /**\n * Reload the month view in this month.\n *\n * @param {EventFacade} e\n */\n var reloadMonth = function(e) {\n var root = e.data;\n var body = $('body');\n var namespace = '.' + root.attr('id');\n\n if (root.is(':visible')) {\n CalendarViewManager.reloadCurrentMonth(root);\n } else {\n // The root has been removed.\n // Remove all events in the namespace.\n body.off(CalendarEvents.created + namespace);\n body.off(CalendarEvents.deleted + namespace);\n body.off(CalendarEvents.updated + namespace);\n body.off(CalendarEvents.eventMoved + namespace);\n }\n };\n\n var registerEventListeners = function(root) {\n $('body').on(CalendarEvents.filterChanged, function(e, data) {\n var daysWithEvent = root.find(CalendarSelectors.eventType[data.type]);\n\n daysWithEvent.toggleClass('calendar_event_' + data.type, !data.hidden);\n });\n\n var namespace = '.' + root.attr('id');\n $('body').on('change' + namespace, CalendarSelectors.elements.courseSelector, function() {\n if (root.is(':visible')) {\n var selectElement = $(this);\n var courseId = selectElement.val();\n var categoryId = null;\n\n CalendarViewManager.reloadCurrentMonth(root, courseId, categoryId);\n } else {\n $('body').off('change' + namespace);\n }\n });\n\n };\n\n return {\n init: function(root, loadOnInit) {\n root = $(root);\n\n CalendarViewManager.init(root);\n registerEventListeners(root);\n registerCalendarEventListeners(root);\n\n if (loadOnInit) {\n // The calendar hasn't yet loaded it's events so we\n // should load them as soon as we've initialised.\n CalendarViewManager.reloadCurrentMonth(root);\n }\n\n }\n };\n});\n"],"names":["define","$","CalendarSelectors","CalendarEvents","CalendarViewManager","reloadMonth","e","root","data","body","namespace","attr","is","reloadCurrentMonth","off","created","deleted","updated","eventMoved","init","loadOnInit","on","filterChanged","find","eventType","type","toggleClass","hidden","elements","courseSelector","courseId","this","val","registerEventListeners","registerCalendarEventListeners"],"mappings":";;;;;;;;;;;AA0BAA,qCAAO,CACH,SACA,0BACA,uBACA,+BAEJ,SACIC,EACAC,kBACAC,eACAC,yBAwBIC,YAAc,SAASC,OACnBC,KAAOD,EAAEE,KACTC,KAAOR,EAAE,QACTS,UAAY,IAAMH,KAAKI,KAAK,MAE5BJ,KAAKK,GAAG,YACRR,oBAAoBS,mBAAmBN,OAIvCE,KAAKK,IAAIX,eAAeY,QAAUL,WAClCD,KAAKK,IAAIX,eAAea,QAAUN,WAClCD,KAAKK,IAAIX,eAAec,QAAUP,WAClCD,KAAKK,IAAIX,eAAee,WAAaR,mBA0BtC,CACHS,KAAM,SAASZ,KAAMa,YACjBb,KAAON,EAAEM,MAETH,oBAAoBe,KAAKZ,MA1BJ,SAASA,MAClCN,EAAE,QAAQoB,GAAGlB,eAAemB,eAAe,SAAShB,EAAGE,MAC/BD,KAAKgB,KAAKrB,kBAAkBsB,UAAUhB,KAAKiB,OAEjDC,YAAY,kBAAoBlB,KAAKiB,MAAOjB,KAAKmB,eAG/DjB,UAAY,IAAMH,KAAKI,KAAK,MAChCV,EAAE,QAAQoB,GAAG,SAAWX,UAAWR,kBAAkB0B,SAASC,gBAAgB,cACtEtB,KAAKK,GAAG,YAAa,KAEjBkB,SADgB7B,EAAE8B,MACOC,MAG7B5B,oBAAoBS,mBAAmBN,KAAMuB,SAF5B,WAIjB7B,EAAE,QAAQa,IAAI,SAAWJ,cAW7BuB,CAAuB1B,MA3DM,SAASA,UACtCE,KAAOR,EAAE,QACTS,UAAY,IAAMH,KAAKI,KAAK,MAEhCF,KAAKY,GAAGlB,eAAeY,QAAUL,UAAWH,KAAMF,aAClDI,KAAKY,GAAGlB,eAAea,QAAUN,UAAWH,KAAMF,aAClDI,KAAKY,GAAGlB,eAAec,QAAUP,UAAWH,KAAMF,aAClDI,KAAKY,GAAGlB,eAAee,WAAaR,UAAWH,KAAMF,aAqDjD6B,CAA+B3B,MAE3Ba,YAGAhB,oBAAoBS,mBAAmBN"}PK*\"amd/build/summary_modal.min.js.mapnu[{"version":3,"file":"summary_modal.min.js","sources":["../src/summary_modal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A javascript module to handle summary modal.\n *\n * @module core_calendar/summary_modal\n * @copyright 2017 Simey Lameze \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport * as CustomEvents from 'core/custom_interaction_events';\nimport Modal from 'core/modal';\nimport CalendarEvents from './events';\nimport * as CalendarCrud from 'core_calendar/crud';\nimport * as ModalEvents from 'core/modal_events';\n\nconst SELECTORS = {\n ROOT: \"[data-region='summary-modal-container']\",\n EDIT_BUTTON: '[data-action=\"edit\"]',\n DELETE_BUTTON: '[data-action=\"delete\"]',\n};\n\nexport default class ModalEventSummary extends Modal {\n static TEMPLATE = 'core_calendar/event_summary_modal';\n static TYPE = 'core_calendar-event_summary';\n\n /**\n * Get the edit button element from the footer. The button is cached\n * as it's not expected to change.\n *\n * @method getEditButton\n * @return {object} button element\n */\n getEditButton() {\n if (typeof this.editButton == 'undefined') {\n this.editButton = this.getFooter().find(SELECTORS.EDIT_BUTTON);\n }\n\n return this.editButton;\n }\n\n /**\n * Get the delete button element from the footer. The button is cached\n * as it's not expected to change.\n *\n * @method getDeleteButton\n * @return {object} button element\n */\n getDeleteButton() {\n if (typeof this.deleteButton == 'undefined') {\n this.deleteButton = this.getFooter().find(SELECTORS.DELETE_BUTTON);\n }\n\n return this.deleteButton;\n }\n\n /**\n * Get the id for the event being shown in this modal. This value is\n * not cached because it will change depending on which event is\n * being displayed.\n *\n * @method getEventId\n * @return {int}\n */\n getEventId() {\n return this.getBody().find(SELECTORS.ROOT).attr('data-event-id');\n }\n\n /**\n * Get the title for the event being shown in this modal. This value is\n * not cached because it will change depending on which event is\n * being displayed.\n *\n * @method getEventTitle\n * @return {String}\n */\n getEventTitle() {\n return this.getBody().find(SELECTORS.ROOT).attr('data-event-title');\n }\n\n /**\n * Get the number of events in the series for the event being shown in\n * this modal. This value is not cached because it will change\n * depending on which event is being displayed.\n *\n * @method getEventCount\n * @return {int}\n */\n getEventCount() {\n return this.getBody().find(SELECTORS.ROOT).attr('data-event-count');\n }\n\n /**\n * Get the url for the event being shown in this modal.\n *\n * @method getEventUrl\n * @return {String}\n */\n getEditUrl() {\n return this.getBody().find(SELECTORS.ROOT).attr('data-edit-url');\n }\n\n /**\n * Is this an action event.\n *\n * @method getEventUrl\n * @return {String}\n */\n isActionEvent() {\n return (this.getBody().find(SELECTORS.ROOT).attr('data-action-event') == 'true');\n }\n\n /**\n * Set up all of the event handling for the modal.\n *\n * @method registerEventListeners\n */\n registerEventListeners() {\n // Apply parent event listeners.\n super.registerEventListeners(this);\n\n // We have to wait for the modal to finish rendering in order to ensure that\n // the data-event-title property is available to use as the modal title.\n M.util.js_pending('core_calendar/summary_modal:registerEventListeners:bodyRendered');\n this.getRoot().on(ModalEvents.bodyRendered, function() {\n this.getModal().data({\n eventTitle: this.getEventTitle(),\n eventId: this.getEventId(),\n eventCount: this.getEventCount(),\n })\n .attr('data-type', 'event');\n CalendarCrud.registerRemove(this.getModal());\n M.util.js_complete('core_calendar/summary_modal:registerEventListeners:bodyRendered');\n }.bind(this));\n\n $('body').on(CalendarEvents.deleted, function() {\n // Close the dialogue on delete.\n this.hide();\n }.bind(this));\n\n CustomEvents.define(this.getEditButton(), [\n CustomEvents.events.activate\n ]);\n\n this.getEditButton().on(CustomEvents.events.activate, function(e, data) {\n if (this.isActionEvent()) {\n // Action events cannot be edited on the event form and must be redirected to the module UI.\n $('body').trigger(CalendarEvents.editActionEvent, [this.getEditUrl()]);\n } else {\n // When the edit button is clicked we fire an event for the calendar UI to handle.\n // We don't care how the UI chooses to handle it.\n $('body').trigger(CalendarEvents.editEvent, [this.getEventId()]);\n }\n\n // There is nothing else for us to do so let's hide.\n this.hide();\n\n // We've handled this event so no need to propagate it.\n e.preventDefault();\n e.stopPropagation();\n data.originalEvent.preventDefault();\n data.originalEvent.stopPropagation();\n }.bind(this));\n }\n}\n\nModalEventSummary.registerModalType();\n"],"names":["SELECTORS","ModalEventSummary","Modal","getEditButton","this","editButton","getFooter","find","getDeleteButton","deleteButton","getEventId","getBody","attr","getEventTitle","getEventCount","getEditUrl","isActionEvent","registerEventListeners","M","util","js_pending","getRoot","on","ModalEvents","bodyRendered","getModal","data","eventTitle","eventId","eventCount","CalendarCrud","registerRemove","js_complete","bind","CalendarEvents","deleted","hide","CustomEvents","define","events","activate","e","trigger","editActionEvent","editEvent","preventDefault","stopPropagation","originalEvent","registerModalType"],"mappings":"6vDA8BMA,eACI,0CADJA,sBAEW,uBAFXA,wBAGa,+BAGEC,0BAA0BC,eAW3CC,4BACkC,IAAnBC,KAAKC,kBACPA,WAAaD,KAAKE,YAAYC,KAAKP,wBAGrCI,KAAKC,WAUhBG,8BACoC,IAArBJ,KAAKK,oBACPA,aAAeL,KAAKE,YAAYC,KAAKP,0BAGvCI,KAAKK,aAWhBC,oBACWN,KAAKO,UAAUJ,KAAKP,gBAAgBY,KAAK,iBAWpDC,uBACWT,KAAKO,UAAUJ,KAAKP,gBAAgBY,KAAK,oBAWpDE,uBACWV,KAAKO,UAAUJ,KAAKP,gBAAgBY,KAAK,oBASpDG,oBACWX,KAAKO,UAAUJ,KAAKP,gBAAgBY,KAAK,iBASpDI,sBAC6E,QAAjEZ,KAAKO,UAAUJ,KAAKP,gBAAgBY,KAAK,qBAQrDK,+BAEUA,uBAAuBb,MAI7Bc,EAAEC,KAAKC,WAAW,wEACbC,UAAUC,GAAGC,YAAYC,aAAc,gBACnCC,WAAWC,KAAK,CACjBC,WAAYvB,KAAKS,gBACjBe,QAASxB,KAAKM,aACdmB,WAAYzB,KAAKU,kBAEpBF,KAAK,YAAa,SACnBkB,aAAaC,eAAe3B,KAAKqB,YACjCP,EAAEC,KAAKa,YAAY,oEACrBC,KAAK7B,2BAEL,QAAQkB,GAAGY,gBAAeC,QAAS,gBAE5BC,QACPH,KAAK7B,OAEPiC,aAAaC,OAAOlC,KAAKD,gBAAiB,CACtCkC,aAAaE,OAAOC,gBAGnBrC,gBAAgBmB,GAAGe,aAAaE,OAAOC,SAAU,SAASC,EAAGf,MAC1DtB,KAAKY,oCAEH,QAAQ0B,QAAQR,gBAAeS,gBAAiB,CAACvC,KAAKW,mCAItD,QAAQ2B,QAAQR,gBAAeU,UAAW,CAACxC,KAAKM,oBAIjD0B,OAGLK,EAAEI,iBACFJ,EAAEK,kBACFpB,KAAKqB,cAAcF,iBACnBnB,KAAKqB,cAAcD,mBACrBb,KAAK7B,kEA5IMH,6BACC,qDADDA,yBAEH,+BA8IlBA,kBAAkB+C"}PK*\#!amd/build/modal_event_form.min.jsnu[define("core_calendar/modal_event_form",["exports","jquery","core/custom_interaction_events","core/modal","core_form/events","./events","core/str","core/notification","core/fragment","core_calendar/repository"],(function(_exports,_jquery,CustomEvents,_modal,FormEvents,_events2,Str,Notification,Fragment,Repository){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),CustomEvents=_interopRequireWildcard(CustomEvents),_modal=_interopRequireDefault(_modal),FormEvents=_interopRequireWildcard(FormEvents),_events2=_interopRequireDefault(_events2),Str=_interopRequireWildcard(Str),Notification=_interopRequireWildcard(Notification),Fragment=_interopRequireWildcard(Fragment),Repository=_interopRequireWildcard(Repository);const SELECTORS_SAVE_BUTTON='[data-action="save"]',SELECTORS_LOADING_ICON_CONTAINER='[data-region="loading-icon-container"]';class ModalEventForm extends _modal.default{constructor(root){super(root),this.eventId=null,this.startTime=null,this.courseId=null,this.categoryId=null,this.contextId=null,this.reloadingBody=!1,this.reloadingTitle=!1,this.saveButton=this.getFooter().find(SELECTORS_SAVE_BUTTON)}configure(modalConfig){modalConfig.large=!0,super.configure(modalConfig)}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setCourseId(id){this.courseId=id}getCourseId(){return this.courseId}setCategoryId(id){this.categoryId=id}getCategoryId(){return this.categoryId}hasCourseId(){return null!==this.courseId}hasCategoryId(){return null!==this.categoryId}setEventId(id){this.eventId=id}getEventId(){return this.eventId}hasEventId(){return null!==this.eventId}setStartTime(time){this.startTime=time}getStartTime(){return this.startTime}hasStartTime(){return null!==this.startTime}getForm(){return this.getBody().find("form")}disableButtons(){this.saveButton.prop("disabled",!0)}enableButtons(){this.saveButton.prop("disabled",!1)}reloadTitleContent(){return this.reloadingTitle||(this.reloadingTitle=!0,this.hasEventId()?this.titlePromise=Str.get_string("editevent","calendar"):this.titlePromise=Str.get_string("newevent","calendar"),this.titlePromise.then((string=>(this.setTitle(string),string))).catch(Notification.exception).always((()=>{this.reloadingTitle=!1}))),this.titlePromise}reloadBodyContent(formData){if(this.reloadingBody)return this.bodyPromise;this.reloadingBody=!0,this.disableButtons();const args={};return this.hasEventId()&&(args.eventid=this.getEventId()),this.hasStartTime()&&(args.starttime=this.getStartTime()),this.hasCourseId()&&(args.courseid=this.getCourseId()),this.hasCategoryId()&&(args.categoryid=this.getCategoryId()),void 0!==formData&&(args.formdata=formData),this.bodyPromise=Fragment.loadFragment("calendar","event_form",this.getContextId(),args),this.setBody(this.bodyPromise),this.bodyPromise.then((()=>{this.enableButtons()})).catch(Notification.exception).always((()=>{this.reloadingBody=!1})),this.bodyPromise}reloadAllContent(){return _jquery.default.when(this.reloadTitleContent(),this.reloadBodyContent())}show(){this.reloadAllContent(),super.show(this)}hide(){super.hide(this),this.setEventId(null),this.setStartTime(null),this.setCourseId(null),this.setCategoryId(null)}getFormData(){return this.getForm().serialize()}save(){const loadingContainer=this.saveButton.find(SELECTORS_LOADING_ICON_CONTAINER),invalid=this.getForm().find('[aria-invalid="true"]');if(invalid.length)return invalid.first().focus(),Promise.resolve();loadingContainer.removeClass("hidden"),this.disableButtons();const formData=this.getFormData();return Repository.submitCreateUpdateForm(formData).then((response=>{if(response.validationerror)this.reloadBodyContent(formData);else{const isExisting=this.hasEventId();this.hide(),isExisting?(0,_jquery.default)("body").trigger(_events2.default.updated,[response.event]):(0,_jquery.default)("body").trigger(_events2.default.created,[response.event])}})).catch(Notification.exception).always((()=>{loadingContainer.addClass("hidden"),this.enableButtons()}))}registerEventListeners(){super.registerEventListeners(this),this.getModal().on(CustomEvents.events.activate,SELECTORS_SAVE_BUTTON,((e,data)=>{this.getForm().submit(),data.originalEvent.preventDefault(),e.stopPropagation()})),this.getModal().on("submit",(e=>{FormEvents.notifyFormSubmittedByJavascript(this.getForm()[0]),this.save(),e.preventDefault(),e.stopPropagation()}))}}return _exports.default=ModalEventForm,_defineProperty(ModalEventForm,"TYPE","core_calendar-modal_event_form"),_defineProperty(ModalEventForm,"TEMPLATE","calendar/modal_event_form"),ModalEventForm.registerModalType(),_exports.default})); //# sourceMappingURL=modal_event_form.min.js.mapPK*\$$/amd/build/month_navigation_drag_drop.min.js.mapnu[{"version":3,"file":"month_navigation_drag_drop.min.js","sources":["../src/month_navigation_drag_drop.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A javascript module to handle calendar drag and drop in the calendar\n * month view navigation.\n *\n * This code is run each time the calendar month view is re-rendered. We\n * only register the event handlers once per page load so that the in place\n * DOM updates that happen on month change don't continue to register handlers.\n *\n * @module core_calendar/month_navigation_drag_drop\n * @copyright 2017 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core_calendar/drag_drop_data_store',\n ],\n function(\n $,\n DataStore\n ) {\n\n var SELECTORS = {\n DRAGGABLE: '[draggable=\"true\"][data-region=\"event-item\"]',\n DROP_ZONE: '[data-drop-zone=\"nav-link\"]',\n };\n var HOVER_CLASS = 'bg-primary text-white';\n var TARGET_CLASS = 'drop-target';\n var HOVER_TIME = 1000; // 1 second hover to change month.\n\n // We store some static variables at the module level because this\n // module is called each time the calendar month view is reloaded but\n // we want some actions to only occur ones.\n\n /* @var {bool} registered If the event listeners have been added */\n var registered = false;\n /* @var {int} hoverTimer The timeout id of any timeout waiting for hover */\n var hoverTimer = null;\n /* @var {object} root The root nav element we're operating on */\n var root = null;\n\n /**\n * Add or remove the appropriate styling to indicate whether\n * the drop target is being hovered over.\n *\n * @param {object} target The target drop zone element\n * @param {bool} hovered If the element is hovered over ot not\n */\n var updateHoverState = function(target, hovered) {\n if (hovered) {\n target.addClass(HOVER_CLASS);\n } else {\n target.removeClass(HOVER_CLASS);\n }\n };\n\n /**\n * Add some styling to the UI to indicate that the nav links\n * are an acceptable drop target.\n */\n var addDropZoneIndicator = function() {\n root.find(SELECTORS.DROP_ZONE).addClass(TARGET_CLASS);\n };\n\n /**\n * Remove the styling from the nav links.\n */\n var removeDropZoneIndicator = function() {\n root.find(SELECTORS.DROP_ZONE).removeClass(TARGET_CLASS);\n };\n\n /**\n * Get the drop zone target from the event, if one is found.\n *\n * @param {event} e Javascript event\n * @return {object|null}\n */\n var getTargetFromEvent = function(e) {\n var target = $(e.target).closest(SELECTORS.DROP_ZONE);\n return (target.length) ? target : null;\n };\n\n /**\n * This will add a visual indicator to the calendar UI to\n * indicate which nav link is a valid drop zone.\n *\n * @param {Event} e\n */\n var dragstartHandler = function(e) {\n // Make sure the drag event is for a calendar event.\n var eventElement = $(e.target).closest(SELECTORS.DRAGGABLE);\n\n if (eventElement.length) {\n addDropZoneIndicator();\n }\n };\n\n /**\n * Update the hover state of the target nav element when\n * the user is dragging an event over it.\n *\n * This will add a visual indicator to the calendar UI to\n * indicate which nav link is being hovered.\n *\n * @param {event} e The dragover event\n */\n var dragoverHandler = function(e) {\n // Ignore dragging of non calendar events.\n if (!DataStore.hasEventId()) {\n return;\n }\n\n e.preventDefault();\n var target = getTargetFromEvent(e);\n\n if (!target) {\n return;\n }\n\n // If we're not draggin a calendar event then\n // ignore it.\n if (!DataStore.hasEventId()) {\n return;\n }\n\n if (!hoverTimer) {\n hoverTimer = setTimeout(function() {\n target.click();\n hoverTimer = null;\n }, HOVER_TIME);\n }\n\n updateHoverState(target, true);\n removeDropZoneIndicator();\n };\n\n /**\n * Update the hover state of the target nav element that was\n * previously dragged over but has is no longer a drag target.\n *\n * This will remove the visual indicator from the calendar UI\n * that was added by the dragoverHandler.\n *\n * @param {event} e The dragstart event\n */\n var dragleaveHandler = function(e) {\n // Ignore dragging of non calendar events.\n if (!DataStore.hasEventId()) {\n return;\n }\n\n var target = getTargetFromEvent(e);\n\n if (!target) {\n return;\n }\n\n if (hoverTimer) {\n clearTimeout(hoverTimer);\n hoverTimer = null;\n }\n\n updateHoverState(target, false);\n addDropZoneIndicator();\n e.preventDefault();\n };\n\n /**\n * Remove the visual indicator from the calendar UI that was\n * added by the dragoverHandler.\n *\n * @param {event} e The drop event\n */\n var dropHandler = function(e) {\n // Ignore dragging of non calendar events.\n if (!DataStore.hasEventId()) {\n return;\n }\n\n removeDropZoneIndicator();\n var target = getTargetFromEvent(e);\n\n if (!target) {\n return;\n }\n\n updateHoverState(target, false);\n e.preventDefault();\n };\n\n return {\n /**\n * Initialise the event handlers for the drag events.\n *\n * @param {object} rootElement The element containing calendar nav links\n */\n init: function(rootElement) {\n // Only register the handlers once on the first load.\n if (!registered) {\n // These handlers are only added the first time the module\n // is loaded because we don't want to have a new listener\n // added each time the \"init\" function is called otherwise we'll\n // end up with lots of stale handlers.\n document.addEventListener('dragstart', dragstartHandler, false);\n document.addEventListener('dragover', dragoverHandler, false);\n document.addEventListener('dragleave', dragleaveHandler, false);\n document.addEventListener('drop', dropHandler, false);\n document.addEventListener('dragend', removeDropZoneIndicator, false);\n registered = true;\n }\n\n // Update the module variable to operate on the given\n // root element.\n root = $(rootElement);\n\n // If we're currently dragging then add the indicators.\n if (DataStore.hasEventId()) {\n addDropZoneIndicator();\n }\n },\n };\n});\n"],"names":["define","$","DataStore","SELECTORS","registered","hoverTimer","root","updateHoverState","target","hovered","addClass","removeClass","addDropZoneIndicator","find","removeDropZoneIndicator","getTargetFromEvent","e","closest","length","dragstartHandler","dragoverHandler","hasEventId","preventDefault","setTimeout","click","dragleaveHandler","clearTimeout","dropHandler","init","rootElement","document","addEventListener"],"mappings":";;;;;;;;;;;;AA2BAA,kDAAO,CACK,SACA,uCAEJ,SACIC,EACAC,eAGJC,oBACW,+CADXA,oBAEW,8BAWXC,YAAa,EAEbC,WAAa,KAEbC,KAAO,KASPC,iBAAmB,SAASC,OAAQC,SAChCA,QACAD,OAAOE,SAxBG,yBA0BVF,OAAOG,YA1BG,0BAkCdC,qBAAuB,WACvBN,KAAKO,KAAKV,qBAAqBO,SAlChB,gBAwCfI,wBAA0B,WAC1BR,KAAKO,KAAKV,qBAAqBQ,YAzChB,gBAkDfI,mBAAqB,SAASC,OAC1BR,OAASP,EAAEe,EAAER,QAAQS,QAAQd,4BACzBK,OAAOU,OAAUV,OAAS,MASlCW,iBAAmB,SAASH,GAETf,EAAEe,EAAER,QAAQS,QAAQd,qBAEtBe,QACbN,wBAaJQ,gBAAkB,SAASJ,MAEtBd,UAAUmB,cAIfL,EAAEM,qBACEd,OAASO,mBAAmBC,GAE3BR,QAMAN,UAAUmB,eAIVhB,aACDA,WAAakB,YAAW,WACpBf,OAAOgB,QACPnB,WAAa,OApGR,MAwGbE,iBAAiBC,QAAQ,GACzBM,6BAYAW,iBAAmB,SAAST,MAEvBd,UAAUmB,kBAIXb,OAASO,mBAAmBC,GAE3BR,SAIDH,aACAqB,aAAarB,YACbA,WAAa,MAGjBE,iBAAiBC,QAAQ,GACzBI,uBACAI,EAAEM,oBASFK,YAAc,SAASX,MAElBd,UAAUmB,cAIfP,8BACIN,OAASO,mBAAmBC,GAE3BR,SAILD,iBAAiBC,QAAQ,GACzBQ,EAAEM,0BAGC,CAMHM,KAAM,SAASC,aAENzB,aAKD0B,SAASC,iBAAiB,YAAaZ,kBAAkB,GACzDW,SAASC,iBAAiB,WAAYX,iBAAiB,GACvDU,SAASC,iBAAiB,YAAaN,kBAAkB,GACzDK,SAASC,iBAAiB,OAAQJ,aAAa,GAC/CG,SAASC,iBAAiB,UAAWjB,yBAAyB,GAC9DV,YAAa,GAKjBE,KAAOL,EAAE4B,aAGL3B,UAAUmB,cACVT"}PK*\q UPGRADING.mdnu[# core_calendar (subsystem) Upgrade notes ## 5.0 ### Deprecated - Initial deprecation of calendar_sub_month. Use \core_calendar\type_factory::get_calendar_instance()->get_prev_month() instead. For more information see [MDL-79434](https://tracker.moodle.org/browse/MDL-79434) - calendar_day_representation(), calendar_time_representation() and calendar_format_event_time() functions have been deprecated and can't be used anymore. Use humandate and humantimeperiod classes instead. For more information see [MDL-83873](https://tracker.moodle.org/browse/MDL-83873) - calendar_get_courselink(), calendar_events_by_day() functions have been deprecated. For more information see [MDL-84617](https://tracker.moodle.org/browse/MDL-84617) - Initial deprecation of calendar_add_month(). Use \core_calendar\type_factory::get_calendar_instance()->get_next_month() instead. For more information see [MDL-84657](https://tracker.moodle.org/browse/MDL-84657) ### Removed - Final removal of calendar functions: - calendar_top_controls() - calendar_get_link_previous() - calendar_get_link_next() For more information see [MDL-79434](https://tracker.moodle.org/browse/MDL-79434) - prepare_for_view(), calendar_add_event_metadata() functions have been removed. For more information see [MDL-84617](https://tracker.moodle.org/browse/MDL-84617) - core_calendar_renderer::event() method has been removed. For more information see [MDL-84617](https://tracker.moodle.org/browse/MDL-84617) PK*\Wܡtests/rrule_manager_test.phpnu[. namespace core_calendar; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); /** * Defines test class to test manage rrule during ical imports. * * @package core_calendar * @category test * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class rrule_manager_test extends \advanced_testcase { /** @var calendar_event a dummy event */ protected $event; /** * Set up method. */ protected function setUp(): void { global $DB; parent::setUp(); $this->resetAfterTest(); // Set our timezone based on the timezone in the RFC's samples (America/New_York). $tz = 'America/New_York'; $this->setTimezone($tz); $timezone = new \DateTimeZone($tz); // Create our event's DTSTART date based on RFC's samples (most commonly used in RFC is 1997-09-02 09:00:00 EDT). $time = \DateTime::createFromFormat('Ymd\THis', '19970902T090000', $timezone); $timestart = $time->getTimestamp(); $user = $this->getDataGenerator()->create_user(); $sub = new \stdClass(); $sub->url = ''; $sub->courseid = 0; $sub->groupid = 0; $sub->userid = $user->id; $sub->pollinterval = 0; $subid = $DB->insert_record('event_subscriptions', $sub, true); $event = new \stdClass(); $event->name = 'Event name'; $event->description = ''; $event->timestart = $timestart; $event->timeduration = 3600; $event->uuid = 'uuid'; $event->subscriptionid = $subid; $event->userid = $user->id; $event->groupid = 0; $event->courseid = 0; $event->eventtype = 'user'; $eventobj = \calendar_event::create($event, false); $DB->set_field('event', 'repeatid', $eventobj->id, array('id' => $eventobj->id)); $eventobj->repeatid = $eventobj->id; $this->event = $eventobj; } /** * Test parse_rrule() method. */ public function test_parse_rrule(): void { $rules = [ 'FREQ=YEARLY', 'COUNT=3', 'INTERVAL=4', 'BYSECOND=20,40', 'BYMINUTE=2,30', 'BYHOUR=3,4', 'BYDAY=MO,TH', 'BYMONTHDAY=20,30', 'BYYEARDAY=300,-20', 'BYWEEKNO=22,33', 'BYMONTH=3,4' ]; $rrule = implode(';', $rules); $mang = new rrule_manager($rrule); $mang->parse_rrule(); $bydayrules = [ (object)[ 'day' => 'MO', 'value' => 0 ], (object)[ 'day' => 'TH', 'value' => 0 ], ]; $props = [ 'freq' => rrule_manager::FREQ_YEARLY, 'count' => 3, 'interval' => 4, 'bysecond' => [20, 40], 'byminute' => [2, 30], 'byhour' => [3, 4], 'byday' => $bydayrules, 'bymonthday' => [20, 30], 'byyearday' => [300, -20], 'byweekno' => [22, 33], 'bymonth' => [3, 4], ]; $reflectionclass = new \ReflectionClass($mang); foreach ($props as $prop => $expectedval) { $rcprop = $reflectionclass->getProperty($prop); $this->assertEquals($expectedval, $rcprop->getValue($mang)); } } /** * Test exception is thrown for invalid property. */ public function test_parse_rrule_validation(): void { $rrule = "RANDOM=PROPERTY;"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test exception is thrown for invalid frequency. */ public function test_freq_validation(): void { $rrule = "FREQ=RANDOMLY;"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of rules with both COUNT and UNTIL parameters. */ public function test_until_count_validation(): void { $until = $this->event->timestart + DAYSECS * 4; $until = date('Y-m-d', $until); $rrule = "FREQ=DAILY;COUNT=2;UNTIL=$until"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of INTERVAL rule. */ public function test_interval_validation(): void { $rrule = "INTERVAL=0"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYSECOND rule. */ public function test_bysecond_validation(): void { $rrule = "BYSECOND=30,45,60"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYMINUTE rule. */ public function test_byminute_validation(): void { $rrule = "BYMINUTE=30,45,60"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYMINUTE rule. */ public function test_byhour_validation(): void { $rrule = "BYHOUR=23,45"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYDAY rule. */ public function test_byday_validation(): void { $rrule = "BYDAY=MO,2SE"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYDAY rule with prefixes. */ public function test_byday_with_prefix_validation(): void { // This is acceptable. $rrule = "FREQ=MONTHLY;BYDAY=-1MO,2SA"; $mang = new rrule_manager($rrule); $mang->parse_rrule(); // This is also acceptable. $rrule = "FREQ=YEARLY;BYDAY=MO,2SA"; $mang = new rrule_manager($rrule); $mang->parse_rrule(); // This is invalid. $rrule = "FREQ=WEEKLY;BYDAY=MO,2SA"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYMONTHDAY rule. */ public function test_bymonthday_upper_bound_validation(): void { $rrule = "BYMONTHDAY=1,32"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYMONTHDAY rule. */ public function test_bymonthday_0_validation(): void { $rrule = "BYMONTHDAY=1,0"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYMONTHDAY rule. */ public function test_bymonthday_lower_bound_validation(): void { $rrule = "BYMONTHDAY=1,-31,-32"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYYEARDAY rule. */ public function test_byyearday_upper_bound_validation(): void { $rrule = "BYYEARDAY=1,366,367"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYYEARDAY rule. */ public function test_byyearday_0_validation(): void { $rrule = "BYYEARDAY=0"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYYEARDAY rule. */ public function test_byyearday_lower_bound_validation(): void { $rrule = "BYYEARDAY=-1,-366,-367"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYWEEKNO rule. */ public function test_non_yearly_freq_with_byweekno(): void { $rrule = "BYWEEKNO=1,53"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYWEEKNO rule. */ public function test_byweekno_upper_bound_validation(): void { $rrule = "FREQ=YEARLY;BYWEEKNO=1,53,54"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYWEEKNO rule. */ public function test_byweekno_0_validation(): void { $rrule = "FREQ=YEARLY;BYWEEKNO=0"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYWEEKNO rule. */ public function test_byweekno_lower_bound_validation(): void { $rrule = "FREQ=YEARLY;BYWEEKNO=-1,-53,-54"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYMONTH rule. */ public function test_bymonth_upper_bound_validation(): void { $rrule = "BYMONTH=1,12,13"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYMONTH rule. */ public function test_bymonth_lower_bound_validation(): void { $rrule = "BYMONTH=0"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYSETPOS rule. */ public function test_bysetpos_without_other_byrules(): void { $rrule = "BYSETPOS=1,366"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYSETPOS rule. */ public function test_bysetpos_upper_bound_validation(): void { $rrule = "BYSETPOS=1,366,367"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYSETPOS rule. */ public function test_bysetpos_0_validation(): void { $rrule = "BYSETPOS=0"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test parsing of BYSETPOS rule. */ public function test_bysetpos_lower_bound_validation(): void { $rrule = "BYSETPOS=-1,-366,-367"; $mang = new rrule_manager($rrule); $this->expectException('moodle_exception'); $mang->parse_rrule(); } /** * Test recurrence rules for daily frequency. */ public function test_daily_events(): void { global $DB; $rrule = 'FREQ=DAILY;COUNT=3'; // This should generate 2 child events + 1 parent. $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $count = $DB->count_records('event', array('repeatid' => $this->event->id)); $this->assertEquals(3, $count); $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => ($this->event->timestart + DAYSECS))); $this->assertTrue($result); $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => ($this->event->timestart + 2 * DAYSECS))); $this->assertTrue($result); $until = $this->event->timestart + DAYSECS * 2; $until = date('Y-m-d', $until); $rrule = "FREQ=DAILY;UNTIL=$until"; // This should generate 1 child event + 1 parent,since by then until bound would be hit. $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $count = $DB->count_records('event', array('repeatid' => $this->event->id)); $this->assertEquals(2, $count); $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => ($this->event->timestart + DAYSECS))); $this->assertTrue($result); $rrule = 'FREQ=DAILY;COUNT=3;INTERVAL=3'; // This should generate 2 child events + 1 parent, every 3rd day. $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $count = $DB->count_records('event', array('repeatid' => $this->event->id)); $this->assertEquals(3, $count); $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => ($this->event->timestart + 3 * DAYSECS))); $this->assertTrue($result); $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => ($this->event->timestart + 6 * DAYSECS))); $this->assertTrue($result); } /** * Every 300 days, forever. */ public function test_every_300_days_forever(): void { global $DB; // Change the start date for forever events to 9am of the current date. $this->change_event_startdate(date('Ymd\T090000')); $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $interval = new \DateInterval('P300D'); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P10Y')); $until = $untildate->getTimestamp(); // Forever event. This should generate events for time() + 10 year period, every 300 days. $rrule = 'FREQ=DAILY;INTERVAL=300'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly. $records = $DB->get_records('event', array('repeatid' => $this->event->id), 'timestart ASC', 0, 100); $expecteddate = clone($startdatetime); $first = true; foreach ($records as $record) { $this->assertLessThanOrEqual($until, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next iteration. $expecteddate->add($interval); // Check UUID. if ($first) { // The first instance of the event contains the UUID. $this->assertEquals('uuid', $record->uuid); $first = false; } else { // Succeeding instances will not contain the UUID. $this->assertEmpty($record->uuid); } } } /** * Test recurrence rules for weekly frequency. */ public function test_weekly_events(): void { global $DB; $rrule = 'FREQ=WEEKLY;COUNT=1'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $count = $DB->count_records('event', array('repeatid' => $this->event->id)); $this->assertEquals(1, $count); for ($i = 0; $i < $count; $i++) { $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => ($this->event->timestart + $i * DAYSECS))); $this->assertTrue($result); } // This much seconds after the start of the day. $offset = $this->event->timestart - mktime(0, 0, 0, date("n", $this->event->timestart), date("j", $this->event->timestart), date("Y", $this->event->timestart)); // This should generate 4 weekly Monday events. $until = $this->event->timestart + WEEKSECS * 4; $until = date('Ymd\This\Z', $until); $rrule = "FREQ=WEEKLY;BYDAY=MO;UNTIL=$until"; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $count = $DB->count_records('event', array('repeatid' => $this->event->id)); $this->assertEquals(4, $count); $timestart = $this->event->timestart; for ($i = 0; $i < $count; $i++) { $timestart = strtotime("+$offset seconds next Monday", $timestart); $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => $timestart)); $this->assertTrue($result); } $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart)); $offsetinterval = $startdatetime->diff($startdate, true); $interval = new \DateInterval('P3W'); // Every 3 weeks on Monday, Wednesday for 2 times. $rrule = 'FREQ=WEEKLY;INTERVAL=3;BYDAY=MO,WE;COUNT=2'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(2, $records); $expecteddate = clone($startdate); $expecteddate->modify('1997-09-03'); foreach ($records as $record) { $expecteddate->add($offsetinterval); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); if (date('D', $record->timestart) === 'Mon') { // Go to the fifth day of this month. $expecteddate->modify('next Wednesday'); } else { // Reset to Monday. $expecteddate->modify('last Monday'); // Go to next period. $expecteddate->add($interval); } } } /** * Test recurrence rules for weekly frequency for RRULE with BYDAY rule set, recurring forever. */ public function test_weekly_byday_forever(): void { global $DB; // Set the next Monday as the starting date of this event. $startdate = new \DateTime('next Monday'); // Change the start date of the parent event. $startdate = $this->change_event_startdate($startdate->format('Ymd\T090000')); // Forever event. This should generate events over time() + 10 year period, every 50 weeks. $rrule = 'FREQ=WEEKLY;BYDAY=MO;INTERVAL=50'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P10Y')); $until = $untildate->getTimestamp(); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $interval = new \DateInterval('P50W'); // First instance of this set of recurring events. $expecteddate = clone($startdate); // Iterate over each record and increment the expected date accordingly. foreach ($records as $record) { $eventdateexpected = $expecteddate->format('Y-m-d H:i:s'); $eventdateactual = date('Y-m-d H:i:s', $record->timestart); $this->assertEquals($eventdateexpected, $eventdateactual); $expecteddate->add($interval); $this->assertLessThanOrEqual($until, $record->timestart); } } /** * Test recurrence rules for monthly frequency for RRULE with COUNT and BYMONTHDAY rules set. */ public function test_monthly_events_with_count_bymonthday(): void { global $DB; $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $interval = new \DateInterval('P1M'); $rrule = "FREQ=MONTHLY;COUNT=3;BYMONTHDAY=2"; // This should generate 3 events in total. $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', array('repeatid' => $this->event->id), 'timestart ASC'); $this->assertCount(3, $records); $expecteddate = clone($startdatetime); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next month. $expecteddate->add($interval); } } /** * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set. */ public function test_monthly_events_with_until_bymonthday(): void { global $DB; // This should generate 10 child event + 1 parent, since by then until bound would be hit. $until = strtotime('+1 day +10 months', $this->event->timestart); $until = date('Ymd\This\Z', $until); $rrule = "FREQ=MONTHLY;BYMONTHDAY=2;UNTIL=$until"; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $count = $DB->count_records('event', ['repeatid' => $this->event->id]); $this->assertEquals(11, $count); for ($i = 0; $i < 11; $i++) { $time = strtotime("+$i month", $this->event->timestart); $result = $DB->record_exists('event', ['repeatid' => $this->event->id, 'timestart' => $time]); $this->assertTrue($result); } } /** * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set. */ public function test_monthly_events_with_until_bymonthday_multi(): void { global $DB; $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart)); $offsetinterval = $startdatetime->diff($startdate, true); $interval = new \DateInterval('P2M'); $untildate = clone($startdatetime); $untildate->add(new \DateInterval('P10M10D')); $until = $untildate->format('Ymd\This\Z'); // This should generate 11 child event + 1 parent, since by then until bound would be hit. $rrule = "FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=2,5;UNTIL=$until"; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(12, $records); $expecteddate = clone($startdate); $expecteddate->add($offsetinterval); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); if (date('j', $record->timestart) == 2) { // Go to the fifth day of this month. $expecteddate->add(new \DateInterval('P3D')); } else { // Reset date to the first day of the month. $expecteddate->modify('first day of this month'); // Go to next month period. $expecteddate->add($interval); // Go to the second day of the next month period. $expecteddate->modify('+1 day'); } } } /** * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY forever. */ public function test_monthly_events_with_bymonthday_forever(): void { global $DB; // Change the start date for forever events to 9am of the 2nd day of the current month and year. $this->change_event_startdate(date('Ym02\T090000')); $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart), new \DateTimeZone('Australia/Perth')); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart), new \DateTimeZone('Australia/Perth')); $offsetinterval = $startdatetime->diff($startdate, true); $interval = new \DateInterval('P12M'); // Forever event. This should generate events over a 10-year period, on 2nd day of the month, every 12 months. $rrule = "FREQ=MONTHLY;INTERVAL=12;BYMONTHDAY=2"; $mang = new rrule_manager($rrule); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $until = $untildate->getTimestamp(); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $expecteddate = clone($startdate); $expecteddate->add($offsetinterval); foreach ($records as $record) { $this->assertLessThanOrEqual($until, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Reset date to the first day of the month. $expecteddate->modify('first day of this month'); // Go to next month period. $expecteddate->add($interval); // Go to the second day of the next month period. $expecteddate->modify('+1 day'); } } /** * Test recurrence rules for monthly frequency for RRULE with COUNT and BYDAY rules set. */ public function test_monthly_events_with_count_byday(): void { global $DB; $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart)); $offsetinterval = $startdatetime->diff($startdate, true); $interval = new \DateInterval('P1M'); $rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=1MO'; // This should generate 3 events in total, first monday of the month. $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // First occurrence of this set of recurring events: 06-10-1997. $expecteddate = clone($startdate); $expecteddate->modify('1997-10-06'); $expecteddate->add($offsetinterval); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next month period. $expecteddate->add($interval); $expecteddate->modify('first Monday of this month'); $expecteddate->add($offsetinterval); } } /** * Test recurrence rules for monthly frequency for RRULE with BYDAY and UNTIL rules set. */ public function test_monthly_events_with_until_byday(): void { global $DB; // This much seconds after the start of the day. $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart)); $offsetinterval = $startdatetime->diff($startdate, true); $untildate = clone($startdatetime); $untildate->add(new \DateInterval('P10M1D')); $until = $untildate->format('Ymd\This\Z'); // This rule should generate 9 events in total from first Monday of October 1997 to first Monday of June 1998. $rrule = "FREQ=MONTHLY;BYDAY=1MO;UNTIL=$until"; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(9, $records); $expecteddate = clone($startdate); $expecteddate->modify('first Monday of October 1997'); foreach ($records as $record) { $expecteddate->add($offsetinterval); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next month. $expecteddate->modify('first day of next month'); // Go to the first Monday of the next month. $expecteddate->modify('first Monday of this month'); } } /** * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set. */ public function test_monthly_events_with_until_byday_multi(): void { global $DB; $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart)); $offsetinterval = $startdatetime->diff($startdate, true); $interval = new \DateInterval('P2M'); $untildate = clone($startdatetime); $untildate->add(new \DateInterval('P10M20D')); $until = $untildate->format('Ymd\This\Z'); // This should generate 11 events from 17 Sep 1997 to 15 Jul 1998. $rrule = "FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,3WE;UNTIL=$until"; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(11, $records); $expecteddate = clone($startdate); $expecteddate->modify('1997-09-17'); foreach ($records as $record) { $expecteddate->add($offsetinterval); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); if (date('D', $record->timestart) === 'Mon') { // Go to the fifth day of this month. $expecteddate->modify('third Wednesday of this month'); } else { // Go to next month period. $expecteddate->add($interval); $expecteddate->modify('first Monday of this month'); } } } /** * Test recurrence rules for monthly frequency for RRULE with BYDAY forever. */ public function test_monthly_events_with_byday_forever(): void { global $DB; // Change the start date for forever events to 9am of the 2nd day of the current month and year. $this->change_event_startdate(date('Ym02\T090000')); $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart), new \DateTimeZone('Australia/Perth')); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart), new \DateTimeZone('Australia/Perth')); $offsetinterval = $startdatetime->diff($startdate, true); $interval = new \DateInterval('P12M'); // Forever event. This should generate events over a 10 year period, on 1st Monday of the month every 12 months. $rrule = "FREQ=MONTHLY;INTERVAL=12;BYDAY=1MO"; $mang = new rrule_manager($rrule); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $until = $untildate->getTimestamp(); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $expecteddate = new \DateTime('first Monday of this month'); // Move to the next interval's first Monday if the calculated start date is after this month's first Monday. if ($expecteddate->getTimestamp() < $startdate->getTimestamp()) { $expecteddate->add($interval); $expecteddate->modify('first Monday of this month'); } foreach ($records as $record) { $expecteddate->add($offsetinterval); $this->assertLessThanOrEqual($until, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next month period. $expecteddate->add($interval); // Reset date to the first Monday of the month. $expecteddate->modify('first Monday of this month'); } } /** * Test recurrence rules for yearly frequency. */ public function test_yearly_events(): void { global $DB; $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart)); $offsetinterval = $startdatetime->diff($startdate, true); $interval = new \DateInterval('P1Y'); $rrule = "FREQ=YEARLY;COUNT=3;BYMONTH=9"; // This should generate 3 events in total. $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(3, $records); $expecteddate = clone($startdatetime); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); } // Create a yearly event, until the time limit is hit. $until = strtotime('+20 day +10 years', $this->event->timestart); $until = date('Ymd\THis\Z', $until); $rrule = "FREQ=YEARLY;BYMONTH=9;UNTIL=$until"; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $count = $DB->count_records('event', array('repeatid' => $this->event->id)); $this->assertEquals(11, $count); for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $yoffset = $i * 2, $time = strtotime("+$yoffset years", $this->event->timestart)) { $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => ($time))); $this->assertTrue($result); } // This should generate 5 events in total, every second year in the given month of the event. $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;COUNT=5"; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $count = $DB->count_records('event', array('repeatid' => $this->event->id)); $this->assertEquals(5, $count); for ($i = 0, $time = $this->event->timestart; $i < 5; $i++, $yoffset = $i * 2, $time = strtotime("+$yoffset years", $this->event->timestart)) { $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => ($time))); $this->assertTrue($result); } $rrule = "FREQ=YEARLY;COUNT=3;BYMONTH=9;BYDAY=1MO"; // This should generate 3 events in total. $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(3, $records); $expecteddate = clone($startdatetime); $expecteddate->modify('first Monday of September 1998'); $expecteddate->add($offsetinterval); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); $monthyear = $expecteddate->format('F Y'); $expecteddate->modify('first Monday of ' . $monthyear); $expecteddate->add($offsetinterval); } // Create a yearly event on the specified month, until the time limit is hit. $untildate = clone($startdatetime); $untildate->add(new \DateInterval('P10Y20D')); $until = $untildate->format('Ymd\THis\Z'); $rrule = "FREQ=YEARLY;BYMONTH=9;UNTIL=$until;BYDAY=1MO"; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); // 10 yearly records every first Monday of September 1998 to first Monday of September 2007. $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(10, $records); $expecteddate = clone($startdatetime); $expecteddate->modify('first Monday of September 1998'); $expecteddate->add($offsetinterval); foreach ($records as $record) { $this->assertLessThanOrEqual($untildate->getTimestamp(), $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); $monthyear = $expecteddate->format('F Y'); $expecteddate->modify('first Monday of ' . $monthyear); $expecteddate->add($offsetinterval); } // This should generate 5 events in total, every second year in the month of September. $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;COUNT=5;BYDAY=1MO"; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); // 5 bi-yearly records every first Monday of September 1998 to first Monday of September 2007. $interval = new \DateInterval('P2Y'); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(5, $records); $expecteddate = clone($startdatetime); $expecteddate->modify('first Monday of September 1999'); $expecteddate->add($offsetinterval); foreach ($records as $record) { $this->assertLessThanOrEqual($untildate->getTimestamp(), $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); $monthyear = $expecteddate->format('F Y'); $expecteddate->modify('first Monday of ' . $monthyear); $expecteddate->add($offsetinterval); } } /** * Test for rrule with FREQ=YEARLY and INTERVAL=2 with BYMONTH rule set, recurring forever. */ public function test_yearly_september_every_two_years_forever(): void { global $DB; // Change the start date for forever events to 9am on the 2nd day of September of the current year. $this->change_event_startdate(date('Y0902\T090000')); $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2"; // Forever event. $mang = new rrule_manager($rrule); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $interval = new \DateInterval('P2Y'); $expecteddate = new \DateTime(date('Y0902\T090000')); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to the next expected date. $expecteddate->add($interval); } } /** * Test for rrule with FREQ=YEARLY with BYMONTH and BYDAY rules set, recurring forever. */ public function test_yearly_bymonth_byday_forever(): void { global $DB; // Change the start date for forever events to the first day of September of the current year at 9am. $this->change_event_startdate(date('Y0901\T090000')); // Every 2 years on the first Monday of September. $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;BYDAY=1MO"; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart)); $offsetinterval = $startdatetime->diff($startdate, true); $interval = new \DateInterval('P2Y'); // First occurrence of this set of events is on the first Monday of September. $expecteddate = clone($startdatetime); $expecteddate->modify('first Monday of September'); $expecteddate->add($offsetinterval); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); $monthyear = $expecteddate->format('F Y'); $expecteddate->modify('first Monday of ' . $monthyear); $expecteddate->add($offsetinterval); } } /** * Test for rrule with FREQ=YEARLY recurring forever. */ public function test_yearly_forever(): void { global $DB; // Change the start date for forever events to 9am of the current date. $this->change_event_startdate(date('Ymd\T090000')); $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $interval = new \DateInterval('P2Y'); $rrule = 'FREQ=YEARLY;INTERVAL=2'; // Forever event. $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $expecteddate = clone($startdatetime); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); } } /******************************************************************************************************************************/ /* Tests based on the examples from the RFC. */ /******************************************************************************************************************************/ /** * Daily for 10 occurrences: * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=DAILY;COUNT=10 * ==> (1997 9:00 AM EDT)September 2-11 */ public function test_daily_count(): void { global $DB; $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $interval = new \DateInterval('P1D'); $rrule = 'FREQ=DAILY;COUNT=10'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(10, $records); $expecteddate = new \DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp())); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); } } /** * Daily until December 24, 1997: * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=DAILY;UNTIL=19971224T000000Z * ==> (1997 9:00 AM EDT)September 2-30;October 1-25 * (1997 9:00 AM EST)October 26-31;November 1-30;December 1-23 */ public function test_daily_until(): void { global $DB; $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $interval = new \DateInterval('P1D'); $untildate = new \DateTime('19971224T000000Z'); $untiltimestamp = $untildate->getTimestamp(); $rrule = 'FREQ=DAILY;UNTIL=19971224T000000Z'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // 113 daily events from 02-09-1997 to 23-12-1997. $this->assertCount(113, $records); $expecteddate = new \DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp())); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); } } /** * Every other day - forever: * * DTSTART;TZID=US-Eastern:[Current date]T090000 * RRULE:FREQ=DAILY;INTERVAL=2 * * Sample results (e.g. in the year 1997): * (1997 9:00 AM EDT)September2,4,6,8...24,26,28,30;October 2,4,6...20,22,24 * (1997 9:00 AM EST)October 26,28,30;November 1,3,5,7...25,27,29;Dec 1,3,... */ public function test_every_other_day_forever(): void { global $DB; // Change the start date for forever events to 9am of the current date in America/New_York time. $this->change_event_startdate(date('Ymd\T090000'), 'America/New_York'); $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $interval = new \DateInterval('P2D'); $rrule = 'FREQ=DAILY;INTERVAL=2'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly. $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $expecteddate = new \DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp())); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); } } /** * Every 10 days, 5 occurrences: * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 * ==> (1997 9:00 AM EDT)September 2,12,22;October 2,12 */ public function test_every_10_days_5_count(): void { global $DB; $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $interval = new \DateInterval('P10D'); $rrule = 'FREQ=DAILY;INTERVAL=10;COUNT=5'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(5, $records); $expecteddate = new \DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp())); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); } } /** * Everyday in January, for 3 years: * * DTSTART;TZID=US-Eastern:19980101T090000 * RRULE:FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA * ==> (1998 9:00 AM EDT)January 1-31 * (1999 9:00 AM EDT)January 1-31 * (2000 9:00 AM EDT)January 1-31 */ public function test_everyday_in_jan_for_3_years_yearly(): void { global $DB; // Change our event's date to 01-01-1998, based on the example from the RFC. $this->change_event_startdate('19980101T090000', 'America/New_York'); $rrule = 'FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // 92 events from 01-01-1998 to 03-01-2000. $this->assertCount(92, $records); $untildate = new \DateTime('20000131T090000Z'); $untiltimestamp = $untildate->getTimestamp(); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); // Assert that the event's date is in January. $this->assertEquals('January', date('F', $record->timestart)); } } /** * Everyday in January, for 3 years: * * DTSTART;TZID=US-Eastern:19980101T090000 * RRULE:FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1 * ==> (1998 9:00 AM EDT)January 1-31 * (1999 9:00 AM EDT)January 1-31 * (2000 9:00 AM EDT)January 1-31 */ public function test_everyday_in_jan_for_3_years_daily(): void { global $DB; // Change our event's date to 01-01-1998, based on the example from the RFC. $this->change_event_startdate('19980101T090000', 'America/New_York'); $rrule = 'FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // 92 events from 01-01-1998 to 03-01-2000. $this->assertCount(92, $records); $untildate = new \DateTime('20000131T090000Z'); $untiltimestamp = $untildate->getTimestamp(); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); // Assert that the event's date is in January. $this->assertEquals('January', date('F', $record->timestart)); } } /** * Weekly for 10 occurrences * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=WEEKLY;COUNT=10 * ==> (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21 * (1997 9:00 AM EST)October 28;November 4 */ public function test_weekly_10_count(): void { global $DB; $interval = new \DateInterval('P1W'); $rrule = 'FREQ=WEEKLY;COUNT=10'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(10, $records); $expecteddate = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); } } /** * Weekly until December 24, 1997. * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z * ==> (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21,28 * (1997 9:00 AM EST)November 4,11,18,25;December 2,9,16,23 */ public function test_weekly_until_24_dec_1997(): void { global $DB; $interval = new \DateInterval('P1W'); $rrule = 'FREQ=WEEKLY;UNTIL=19971224T000000Z'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC. $this->assertCount(17, $records); $untildate = new \DateTime('19971224T000000Z'); $untiltimestamp = $untildate->getTimestamp(); $expecteddate = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); } } /** * Every other week - forever: * * DTSTART;TZID=US-Eastern:[Current date]T090000 * RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU * * Sample results (e.g. in the year 1997): * (1997 9:00 AM EDT)September 2,16,30;October 14 * (1997 9:00 AM EST)October 28;November 11,25;December 9,23 * (1998 9:00 AM EST)January 6,20;February * ... */ public function test_every_other_week_forever(): void { global $DB; // Change the start date for forever events to 9am of the current date in America/New_York time. $this->change_event_startdate(date('Ymd\T090000'), 'America/New_York'); $interval = new \DateInterval('P2W'); $rrule = 'FREQ=WEEKLY;INTERVAL=2;WKST=SU'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly. $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $expecteddate = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); } } /** * Weekly on Tuesday and Thursday for 5 weeks: * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH * ==> (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2 */ public function test_weekly_on_tue_thu_for_5_weeks_by_until(): void { global $DB; $rrule = 'FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC. $this->assertCount(10, $records); $untildate = new \DateTime('19971007T000000Z'); $untiltimestamp = $untildate->getTimestamp(); $expecteddate = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $startdate = new \DateTime($expecteddate->format('Y-m-d')); $offset = $expecteddate->diff($startdate, true); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. if ($expecteddate->format('l') === rrule_manager::DAY_TUESDAY) { $expecteddate->modify('next Thursday'); } else { $expecteddate->modify('next Tuesday'); } $expecteddate->add($offset); } } /** * Weekly on Tuesday and Thursday for 5 weeks: * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH * ==> (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2 */ public function test_weekly_on_tue_thu_for_5_weeks_by_count(): void { global $DB; $rrule = 'FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC. $this->assertCount(10, $records); $expecteddate = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $startdate = new \DateTime($expecteddate->format('Y-m-d')); $offset = $expecteddate->diff($startdate, true); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. if ($expecteddate->format('l') === rrule_manager::DAY_TUESDAY) { $expecteddate->modify('next Thursday'); } else { $expecteddate->modify('next Tuesday'); } $expecteddate->add($offset); } } /** * Every other week on Monday, Wednesday and Friday until December 24, 1997, but starting on Tuesday, September 2, 1997: * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR * ==> (1997 9:00 AM EDT)September 3,5,15,17,19,29;October 1,3,13,15,17 * (1997 9:00 AM EST)October 27,29,31;November 10,12,14,24,26,28;December 8,10,12,22 */ public function test_every_other_week_until_24_dec_1997_byday(): void { global $DB; $rrule = 'FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // 24 iterations every M-W-F from 03-09-1997 13:00 UTC to 22-12-1997 13:00 UTC. $this->assertCount(24, $records); $untildate = new \DateTime('19971224T000000Z'); $untiltimestamp = $untildate->getTimestamp(); $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart)); $offsetinterval = $startdatetime->diff($startdate, true); // First occurrence of this set of events is on 3 September 1999. $expecteddate = clone($startdatetime); $expecteddate->modify('next Wednesday'); $expecteddate->add($offsetinterval); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. switch ($expecteddate->format('l')) { case rrule_manager::DAY_MONDAY: $expecteddate->modify('next Wednesday'); break; case rrule_manager::DAY_WEDNESDAY: $expecteddate->modify('next Friday'); break; default: $expecteddate->modify('next Monday'); // Increment expected date by 1 week if the next day is Monday. $expecteddate->add(new \DateInterval('P1W')); break; } $expecteddate->add($offsetinterval); } } /** * Every other week on Tuesday and Thursday, for 8 occurrences: * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH * ==> (1997 9:00 AM EDT)September 2,4,16,18,30;October 2,14,16 */ public function test_every_other_week_byday_8_count(): void { global $DB; $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // Should correspond to COUNT rule. $this->assertCount(8, $records); $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart)); $offsetinterval = $startdatetime->diff($startdate, true); // First occurrence of this set of events is on 2 September 1999. $expecteddate = clone($startdatetime); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. switch ($expecteddate->format('l')) { case rrule_manager::DAY_TUESDAY: $expecteddate->modify('next Thursday'); break; default: $expecteddate->modify('next Tuesday'); // Increment expected date by 1 week if the next day is Tuesday. $expecteddate->add(new \DateInterval('P1W')); break; } $expecteddate->add($offsetinterval); } } /** * Monthly on the 1st Friday for ten occurrences: * * DTSTART;TZID=US-Eastern:19970905T090000 * RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR * ==> (1997 9:00 AM EDT)September 5;October 3 * (1997 9:00 AM EST)November 7;Dec 5 * (1998 9:00 AM EST)January 2;February 6;March 6;April 3 * (1998 9:00 AM EDT)May 1;June 5 */ public function test_monthly_every_first_friday_10_count(): void { global $DB; // Change our event's date to 05-09-1997, based on the example from the RFC. $startdatetime = $this->change_event_startdate('19970905T090000', 'America/New_York'); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart)); $offsetinterval = $startdatetime->diff($startdate, true); $rrule = 'FREQ=MONTHLY;COUNT=10;BYDAY=1FR'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // Should correspond to COUNT rule. $this->assertCount(10, $records); foreach ($records as $record) { // Get the first Friday of the record's month. $recordmonthyear = date('F Y', $record->timestart); $expecteddate = new \DateTime('first Friday of ' . $recordmonthyear); // Add the time of the event. $expecteddate->add($offsetinterval); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); } } /** * Monthly on the 1st Friday until December 24, 1997: * * DTSTART;TZID=US-Eastern:19970905T090000 * RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR * ==> (1997 9:00 AM EDT)September 5;October 3 * (1997 9:00 AM EST)November 7;December 5 */ public function test_monthly_every_first_friday_until(): void { global $DB; // Change our event's date to 05-09-1997, based on the example from the RFC. $startdatetime = $this->change_event_startdate('19970905T090000', 'America/New_York'); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart)); $offsetinterval = $startdatetime->diff($startdate, true); $rrule = 'FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // Should have 4 events, every first friday of September 1997 to December 1997. $this->assertCount(4, $records); foreach ($records as $record) { // Get the first Friday of the record's month. $recordmonthyear = date('F Y', $record->timestart); $expecteddate = new \DateTime('first Friday of ' . $recordmonthyear); // Add the time of the event. $expecteddate->add($offsetinterval); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); } } /** * Every other month on the 1st and last Sunday of the month for 10 occurrences: * * DTSTART;TZID=US-Eastern:19970907T090000 * RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU * ==> (1997 9:00 AM EDT)September 7,28 * (1997 9:00 AM EST)November 2,30 * (1998 9:00 AM EST)January 4,25;March 1,29 * (1998 9:00 AM EDT)May 3,31 */ public function test_every_other_month_1st_and_last_sunday_10_count(): void { global $DB; // Change our event's date to 05-09-1997, based on the example from the RFC. $startdatetime = $this->change_event_startdate('19970907T090000', 'America/New_York'); $startdate = new \DateTime(date('Y-m-d', $this->event->timestart)); $offsetinterval = $startdatetime->diff($startdate, true); $rrule = 'FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // Should have 10 records based on COUNT rule. $this->assertCount(10, $records); // First occurrence is 07-09-1997 which is the first Sunday. $ordinal = 'first'; foreach ($records as $record) { // Get date of the month's first/last Sunday. $recordmonthyear = date('F Y', $record->timestart); $expecteddate = new \DateTime($ordinal . ' Sunday of ' . $recordmonthyear); $expecteddate->add($offsetinterval); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); if ($ordinal === 'first') { $ordinal = 'last'; } else { $ordinal = 'first'; } } } /** * Monthly on the second to last Monday of the month for 6 months: * * DTSTART;TZID=US-Eastern:19970922T090000 * RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO * ==> (1997 9:00 AM EDT)September 22;October 20 * (1997 9:00 AM EST)November 17;December 22 * (1998 9:00 AM EST)January 19;February 16 */ public function test_monthly_last_monday_for_6_months(): void { global $DB; // Change our event's date to 05-09-1997, based on the example from the RFC. $startdatetime = $this->change_event_startdate('19970922T090000', 'America/New_York'); $startdate = new \DateTime($startdatetime->format('Y-m-d')); $offsetinterval = $startdatetime->diff($startdate, true); $rrule = 'FREQ=MONTHLY;COUNT=6;BYDAY=-2MO'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // Should have 6 records based on COUNT rule. $this->assertCount(6, $records); foreach ($records as $record) { // Get date of the month's last Monday. $recordmonthyear = date('F Y', $record->timestart); $expecteddate = new \DateTime('last Monday of ' . $recordmonthyear); // Modify to get the second to the last Monday. $expecteddate->modify('last Monday'); // Add offset. $expecteddate->add($offsetinterval); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); } } /** * Monthly on the third to the last day of the month, forever: * * DTSTART;TZID=US-Eastern:[Current year]0928T090000 * RRULE:FREQ=MONTHLY;BYMONTHDAY=-3 * * Sample results (e.g. in the year 1997): * (1997 9:00 AM EDT)September 28 * (1997 9:00 AM EST)October 29;November 28;December 29 * (1998 9:00 AM EST)January 29;February 26 * ... */ public function test_third_to_the_last_day_of_the_month_forever(): void { global $DB; // Change our event's date to 28 September of the current year, based on the example from the RFC. $this->change_event_startdate(date('Y0928\T090000'), 'America/New_York'); $rrule = 'FREQ=MONTHLY;BYMONTHDAY=-3'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly. $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $subinterval = new \DateInterval('P2D'); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); // Get date of the third to the last day of the month. $recordmonthyear = date('F Y', $record->timestart); $expecteddate = new \DateTime('last day of ' . $recordmonthyear); // Set time to 9am. $expecteddate->setTime(9, 0); // Modify to get the third to the last day of the month. $expecteddate->sub($subinterval); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); } } /** * Monthly on the 2nd and 15th of the month for 10 occurrences: * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15 * ==> (1997 9:00 AM EDT)September 2,15;October 2,15 * (1997 9:00 AM EST)November 2,15;December 2,15 * (1998 9:00 AM EST)January 2,15 */ public function test_every_2nd_and_15th_of_the_month_10_count(): void { global $DB; $startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $startdate = new \DateTime($startdatetime->format('Y-m-d')); $offsetinterval = $startdatetime->diff($startdate, true); $rrule = 'FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // Should have 10 records based on COUNT rule. $this->assertCount(10, $records); $day = '02'; foreach ($records as $record) { // Get the first Friday of the record's month. $recordmonthyear = date('Y-m', $record->timestart); // Get date of the month's last Monday. $expecteddate = new \DateTime("$recordmonthyear-$day"); // Add offset. $expecteddate->add($offsetinterval); if ($day === '02') { $day = '15'; } else { $day = '02'; } $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); } } /** * Monthly on the first and last day of the month for 10 occurrences: * * DTSTART;TZID=US-Eastern:19970930T090000 * RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1 * ==> (1997 9:00 AM EDT)September 30;October 1 * (1997 9:00 AM EST)October 31;November 1,30;December 1,31 * (1998 9:00 AM EST)January 1,31;February 1 */ public function test_every_first_and_last_day_of_the_month_10_count(): void { global $DB; $startdatetime = $this->change_event_startdate('19970930T090000', 'America/New_York'); $startdate = new \DateTime($startdatetime->format('Y-m-d')); $offsetinterval = $startdatetime->diff($startdate, true); $rrule = 'FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // Should have 10 records based on COUNT rule. $this->assertCount(10, $records); // First occurrence is 30-Sep-1997. $day = 'last'; foreach ($records as $record) { // Get the first Friday of the record's month. $recordmonthyear = date('F Y', $record->timestart); // Get date of the month's last Monday. $expecteddate = new \DateTime("$day day of $recordmonthyear"); // Add offset. $expecteddate->add($offsetinterval); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); if ($day === 'first') { $day = 'last'; } else { $day = 'first'; } } } /** * Every 18 months on the 10th thru 15th of the month for 10 occurrences: * * DTSTART;TZID=US-Eastern:19970910T090000 * RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15 * ==> (1997 9:00 AM EDT)September 10,11,12,13,14,15 * (1999 9:00 AM EST)March 10,11,12,13 */ public function test_every_18_months_days_10_to_15_10_count(): void { global $DB; $startdatetime = $this->change_event_startdate('19970910T090000', 'America/New_York'); $rrule = 'FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // Should have 10 records based on COUNT rule. $this->assertCount(10, $records); // First occurrence is 10-Sep-1997. $expecteddate = clone($startdatetime); $expecteddate->setTimezone(new \DateTimeZone(get_user_timezone())); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Get next expected date. if ($expecteddate->format('d') == 15) { // If 15th, increment by 18 months. $expecteddate->add(new \DateInterval('P18M')); // Then go back to the 10th. $expecteddate->sub(new \DateInterval('P5D')); } else { // Otherwise, increment by 1 day. $expecteddate->add(new \DateInterval('P1D')); } } } /** * Every Tuesday, every other month: * * DTSTART;TZID=US-Eastern:[Next Tuesday]T090000 * RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU * * Sample results (e.g. in the year 1997): * (1997 9:00 AM EDT)September 2,9,16,23,30 * (1997 9:00 AM EST)November 4,11,18,25 * (1998 9:00 AM EST)January 6,13,20,27;March 3,10,17,24,31 * ... */ public function test_every_tuesday_every_other_month_forever(): void { global $DB; // Change the start date for forever events to 9am of the Tuesday on or before of the current date in America/New_York time. $nexttuesday = new \DateTime('next Tuesday'); $this->change_event_startdate($nexttuesday->format('Ymd\T090000'), 'America/New_York'); $rrule = 'FREQ=MONTHLY;INTERVAL=2;BYDAY=TU'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly. $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $expecteddate = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart)); $nextmonth = new \DateTime($expecteddate->format('Y-m-d')); $offset = $expecteddate->diff($nextmonth, true); $nextmonth->modify('first day of next month'); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Get next expected date. $expecteddate->modify('next Tuesday'); if ($expecteddate->getTimestamp() >= $nextmonth->getTimestamp()) { // Go to the end of the month. $expecteddate->modify('last day of this month'); // Find the next Tuesday. $expecteddate->modify('next Tuesday'); // Increment next month by 2 months. $nextmonth->add(new \DateInterval('P2M')); } $expecteddate->add($offset); } } /** * Yearly in June and July for 10 occurrences: * * DTSTART;TZID=US-Eastern:19970610T090000 * RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7 * ==> (1997 9:00 AM EDT)June 10;July 10 * (1998 9:00 AM EDT)June 10;July 10 * (1999 9:00 AM EDT)June 10;July 10 * (2000 9:00 AM EDT)June 10;July 10 * (2001 9:00 AM EDT)June 10;July 10 * Note: Since none of the BYDAY, BYMONTHDAY or BYYEARDAY components are specified, the day is gotten from DTSTART. */ public function test_yearly_in_june_july_10_count(): void { global $DB; $startdatetime = $this->change_event_startdate('19970610T090000', 'America/New_York'); $rrule = 'FREQ=YEARLY;COUNT=10;BYMONTH=6,7'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // Should have 10 records based on COUNT rule. $this->assertCount(10, $records); $expecteddate = $startdatetime; $expecteddate->setTimezone(new \DateTimeZone(get_user_timezone())); $monthinterval = new \DateInterval('P1M'); $yearinterval = new \DateInterval('P1Y'); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Get next expected date. if ($expecteddate->format('m') == 6) { // Go to the month of July. $expecteddate->add($monthinterval); } else { // Go to the month of June next year. $expecteddate->sub($monthinterval); $expecteddate->add($yearinterval); } } } /** * Every other year on January, February, and March for 10 occurrences: * * DTSTART;TZID=US-Eastern:19970310T090000 * RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3 * ==> (1997 9:00 AM EST)March 10 * (1999 9:00 AM EST)January 10;February 10;March 10 * (2001 9:00 AM EST)January 10;February 10;March 10 * (2003 9:00 AM EST)January 10;February 10;March 10 */ public function test_every_other_year_in_june_july_10_count(): void { global $DB; $startdatetime = $this->change_event_startdate('19970310T090000', 'America/New_York'); $rrule = 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // Should have 10 records based on COUNT rule. $this->assertCount(10, $records); $expecteddate = $startdatetime; $expecteddate->setTimezone(new \DateTimeZone(get_user_timezone())); $monthinterval = new \DateInterval('P1M'); $yearinterval = new \DateInterval('P2Y'); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Get next expected date. if ($expecteddate->format('m') != 3) { // Go to the next month. $expecteddate->add($monthinterval); } else { // Go to the month of January next year. $expecteddate->sub($monthinterval); $expecteddate->sub($monthinterval); $expecteddate->add($yearinterval); } } } /** * Every 3rd year on the 1st, 100th and 200th day for 10 occurrences: * * DTSTART;TZID=US-Eastern:19970101T090000 * RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200 * ==> (1997 9:00 AM EST)January 1 * (1997 9:00 AM EDT)April 10;July 19 * (2000 9:00 AM EST)January 1 * (2000 9:00 AM EDT)April 9;July 18 * (2003 9:00 AM EST)January 1 * (2003 9:00 AM EDT)April 10;July 19 * (2006 9:00 AM EST)January 1 */ public function test_every_3_years_1st_100th_200th_days_10_count(): void { global $DB; $startdatetime = $this->change_event_startdate('19970101T090000', 'America/New_York'); $rrule = 'FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); // Should have 10 records based on COUNT rule. $this->assertCount(10, $records); $expecteddate = $startdatetime; $expecteddate->setTimezone(new \DateTimeZone(get_user_timezone())); $hundredthdayinterval = new \DateInterval('P99D'); $twohundredthdayinterval = new \DateInterval('P100D'); $yearinterval = new \DateInterval('P3Y'); foreach ($records as $record) { $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Get next expected date. if ($expecteddate->format('z') == 0) { // January 1. $expecteddate->add($hundredthdayinterval); } else if ($expecteddate->format('z') == 99) { // 100th day of the year. $expecteddate->add($twohundredthdayinterval); } else { // 200th day of the year. $expecteddate->add($yearinterval); $expecteddate->modify('January 1'); } } } /** * Every 20th Monday of the year, forever: * * DTSTART;TZID=US-Eastern:[20th Monday of the current year]T090000 * RRULE:FREQ=YEARLY;BYDAY=20MO * * Sample results (e.g. in the year 1997): * (1997 9:00 AM EDT)May 19 * (1998 9:00 AM EDT)May 18 * (1999 9:00 AM EDT)May 17 * ... */ public function test_yearly_every_20th_monday_forever(): void { global $DB; // Change our event's date to the 20th Monday of the current year. $twentiethmonday = new \DateTime(date('Y-01-01')); $twentiethmonday->modify('+20 Monday'); $startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'America/New_York'); $interval = new \DateInterval('P1Y'); $rrule = 'FREQ=YEARLY;BYDAY=20MO'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $expecteddate = $startdatetime; $expecteddate->setTimezone(new \DateTimeZone(get_user_timezone())); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->modify('January 1'); $expecteddate->add($interval); $expecteddate->modify("+20 Monday"); } } /** * Monday of week number 20 (where the default start of the week is Monday), forever: * * DTSTART;TZID=US-Eastern:[1st day of the 20th week this year]T090000 * RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO * * Sample results (e.g. in the year 1997): * (1997 9:00 AM EDT)May 12 * (1998 9:00 AM EDT)May 11 * (1999 9:00 AM EDT)May 17 * ... */ public function test_yearly_byweekno_forever(): void { global $DB; // Change our event's date to the start of the 20th week of the current year. $twentiethweek = new \DateTime(date('Y-01-01')); $twentiethweek->setISODate($twentiethweek->format('Y'), 20); $startdatetime = $this->change_event_startdate($twentiethweek->format('Ymd\T090000'), 'America/New_York'); $startdate = clone($startdatetime); $startdate->modify($startdate->format('Y-m-d')); $offset = $startdatetime->diff($startdate, true); $interval = new \DateInterval('P1Y'); $rrule = 'FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $expecteddate = new \DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp())); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->add($interval); $expecteddate->setISODate($expecteddate->format('Y'), 20); $expecteddate->add($offset); } } /** * Every Thursday in March, forever: * * DTSTART;TZID=US-Eastern:[First thursday of March of the current year]T090000 * RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH * * Sample results (e.g. in the year 1997): * (1997 9:00 AM EST)March 13,20,27 * (1998 9:00 AM EST)March 5,12,19,26 * (1999 9:00 AM EST)March 4,11,18,25 * ... */ public function test_every_thursday_in_march_forever(): void { global $DB; // Change our event's date to the first Thursday of March of the current year at 9am America/New_York time. $firstthursdayofmarch = new \DateTime('first Thursday of March'); $startdatetime = $this->change_event_startdate($firstthursdayofmarch->format('Ymd\T090000'), 'America/New_York'); $interval = new \DateInterval('P1Y'); $rrule = 'FREQ=YEARLY;BYMONTH=3;BYDAY=TH'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly. $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $expecteddate = $startdatetime; $startdate = new \DateTime($startdatetime->format('Y-m-d')); $offsetinterval = $startdatetime->diff($startdate, true); $expecteddate->setTimezone(new \DateTimeZone(get_user_timezone())); $april1st = new \DateTime('April 1'); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->modify('next Thursday'); if ($expecteddate->getTimestamp() >= $april1st->getTimestamp()) { // Reset to 1st of March. $expecteddate->modify('first day of March'); // Go to next year. $expecteddate->add($interval); if ($expecteddate->format('l') !== rrule_manager::DAY_THURSDAY) { $expecteddate->modify('next Thursday'); } // Increment to next year's April 1st. $april1st->add($interval); } $expecteddate->add($offsetinterval); } } /** * Every Thursday, but only during June, July, and August, forever: * * DTSTART;TZID=US-Eastern:[First Thursday of June of the current year]T090000 * RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8 * * Sample results (e.g. in the year 1997): * (1997 9:00 AM EDT)June 5,12,19,26;July 3,10,17,24,31;August 7,14,21,28 * (1998 9:00 AM EDT)June 4,11,18,25;July 2,9,16,23,30;August 6,13,20,27 * (1999 9:00 AM EDT)June 3,10,17,24;July 1,8,15,22,29;August 5,12,19,26 * ... */ public function test_every_thursday_june_july_august_forever(): void { global $DB; // Change our event's date to the first Thursday of June in the current year at 9am America/New_York time. $firstthursdayofjune = new \DateTime('first Thursday of June'); $startdatetime = $this->change_event_startdate($firstthursdayofjune->format('Ymd\T090000'), 'America/New_York'); $startdate = new \DateTime($startdatetime->format('Y-m-d')); $offset = $startdatetime->diff($startdate, true); $interval = new \DateInterval('P1Y'); $rrule = 'FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly. $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $expecteddate = new \DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp())); $september1st = new \DateTime('September 1'); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Go to next period. $expecteddate->modify('next Thursday'); if ($expecteddate->getTimestamp() >= $september1st->getTimestamp()) { $expecteddate->add($interval); $expecteddate->modify('June 1'); if ($expecteddate->format('l') !== rrule_manager::DAY_THURSDAY) { $expecteddate->modify('next Thursday'); } $september1st->add($interval); } $expecteddate->add($offset); } } /** * Every Friday the 13th, forever: * * DTSTART;TZID=US-Eastern:[Current date]T090000 * RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13 * * Sample results (e.g. in the year 1997): * (1998 9:00 AM EST)February 13;March 13;November 13 * (1999 9:00 AM EDT)August 13 * (2000 9:00 AM EDT)October 13 * ... */ public function test_friday_the_thirteenth_forever(): void { global $DB; // Change our event's date to the first Thursday of June in the current year at 9am America/New_York time. $this->change_event_startdate(date('Ymd\T090000'), 'America/New_York'); $rrule = 'FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly. $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); // Assert that the day of the month and the day correspond to Friday the 13th. $this->assertEquals('Friday 13', date('l d', $record->timestart)); } } /** * The first Saturday that follows the first Sunday of the month, forever: * * DTSTART;TZID=US-Eastern:[The Saturday after the month's first Sunday]T090000 * RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13 * * Sample results (e.g. from 13 September 1997): * (1997 9:00 AM EDT)September 13;October 11 * (1997 9:00 AM EST)November 8;December 13 * (1998 9:00 AM EST)January 10;February 7;March 7 * (1998 9:00 AM EDT)April 11;May 9;June 13... */ public function test_first_saturday_following_first_sunday_forever(): void { global $DB; // Change our event's date to the next Saturday after the first Sunday of the current month at 9am America/New_York time. $firstsaturdayafterfirstsunday = new \DateTime('first Sunday of this month'); $firstsaturdayafterfirstsunday->modify('next Saturday'); $startdatetime = $this->change_event_startdate($firstsaturdayafterfirstsunday->format('Ymd\T090000'), 'America/New_York'); $startdate = new \DateTime($startdatetime->format('Y-m-d')); $offset = $startdatetime->diff($startdate, true); $rrule = 'FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly. $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $bymonthdays = [7, 8, 9, 10, 11, 12, 13]; foreach ($records as $record) { $recordmonthyear = date('F Y', $record->timestart); $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); // Get first Saturday after the first Sunday of the month. $expecteddate = new \DateTime('first Sunday of ' . $recordmonthyear); $expecteddate->modify('next Saturday'); $expecteddate->add($offset); // Assert the record's date corresponds to the first Saturday of the month. $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Assert that the record is either the 7th, 8th, 9th, ... 13th day of the month. $this->assertContainsEquals(date('j', $record->timestart), $bymonthdays); } } /** * Every four years, the first Tuesday after a Monday in November, forever (U.S. Presidential Election day): * * DTSTART;TZID=US-Eastern:[Most recent election date]T090000 * RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8 * * Sample results (e.g. from 05 November 1996): * (1996 9:00 AM EST)November 5 * (2000 9:00 AM EST)November 7 * (2004 9:00 AM EST)November 2 * ... */ public function test_every_us_presidential_election_forever(): void { global $DB; // Calculate the most recent election date, starting from 1996 (e.g. today's 2017 so the most recent election was in 2016). $currentyear = (int) date('Y'); $electionyear = 1996; while ($electionyear + 4 < $currentyear) { $electionyear += 4; } $electiondate = new \DateTime('first Monday of November ' . $electionyear); $electiondate->modify('+1 Tuesday'); // Use the most recent election date as the starting date of our recurring events. $startdatetime = $this->change_event_startdate($electiondate->format('Ymd\T090000'), 'America/New_York'); $startdate = new \DateTime($startdatetime->format('Y-m-d')); $offset = $startdatetime->diff($startdate, true); $rrule = 'FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $bymonthdays = [2, 3, 4, 5, 6, 7, 8]; foreach ($records as $record) { $recordmonthyear = date('F Y', $record->timestart); $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); // Get first Saturday after the first Sunday of the month. $expecteddate = new \DateTime('first Monday of ' . $recordmonthyear); $expecteddate->modify('next Tuesday'); $expecteddate->add($offset); // Assert the record's date corresponds to the first Saturday of the month. $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart)); // Assert that the record is either the 2nd, 3rd, 4th ... 8th day of the month. $this->assertContainsEquals(date('j', $record->timestart), $bymonthdays); } } /** * The 3rd instance into the month of one of Tuesday, Wednesday or Thursday, for the next 3 months: * * DTSTART;TZID=US-Eastern:19970904T090000 * RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3 * ==> (1997 9:00 AM EDT)September 4;October 7 * (1997 9:00 AM EST)November 6 */ public function test_monthly_bysetpos_3_count(): void { global $DB; $this->change_event_startdate('19970904T090000', 'America/New_York'); $rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(3, $records); $expecteddates = [ (new \DateTime('1997-09-04 09:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-10-07 09:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-11-06 09:00:00 EST'))->getTimestamp() ]; foreach ($records as $record) { $this->assertContainsEquals($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.'); } } /** * The 2nd to last weekday of the month: * * DTSTART;TZID=US-Eastern:19970929T090000 * RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;COUNT=7 * ==> (1997 9:00 AM EDT)September 29 * (1997 9:00 AM EST)October 30;November 27;December 30 * (1998 9:00 AM EST)January 29;February 26;March 30 * ... * * (Original RFC example is set to recur forever. But we just want to verify that the results match the dates listed from * the RFC example. So just limit the count to 7.) */ public function test_second_to_the_last_weekday_of_the_month(): void { global $DB; $this->change_event_startdate('19970929T090000', 'America/New_York'); $rrule = 'FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;COUNT=7'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); // Get the first 7 samples. This should be enough to verify that we have generated the recurring events correctly. $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 7); $expecteddates = [ (new \DateTime('1997-09-29 09:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-10-30 09:00:00 EST'))->getTimestamp(), (new \DateTime('1997-11-27 09:00:00 EST'))->getTimestamp(), (new \DateTime('1997-12-30 09:00:00 EST'))->getTimestamp(), (new \DateTime('1998-01-29 09:00:00 EST'))->getTimestamp(), (new \DateTime('1998-02-26 09:00:00 EST'))->getTimestamp(), (new \DateTime('1998-03-30 09:00:00 EST'))->getTimestamp(), ]; $untildate = new \DateTime(); $untildate->add(new \DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y')); $untiltimestamp = $untildate->getTimestamp(); $i = 0; foreach ($records as $record) { $this->assertLessThanOrEqual($untiltimestamp, $record->timestart); // Confirm that the first 7 records correspond to the expected dates listed above. $this->assertEquals($expecteddates[$i], $record->timestart); $i++; } } /** * Every 3 hours from 9:00 AM to 5:00 PM on a specific day: * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T210000Z * ==> (September 2, 1997 EDT)09:00,12:00,15:00 */ public function test_every_3hours_9am_to_5pm(): void { global $DB; $rrule = 'FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T210000Z'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(3, $records); $expecteddates = [ (new \DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-09-02 12:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-09-02 15:00:00 EDT'))->getTimestamp(), ]; foreach ($records as $record) { $this->assertContainsEquals($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.'); } } /** * Every 15 minutes for 6 occurrences: * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6 * ==> (September 2, 1997 EDT)09:00,09:15,09:30,09:45,10:00,10:15 */ public function test_every_15minutes_6_count(): void { global $DB; $rrule = 'FREQ=MINUTELY;INTERVAL=15;COUNT=6'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(6, $records); $expecteddates = [ (new \DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-09-02 09:15:00 EDT'))->getTimestamp(), (new \DateTime('1997-09-02 09:30:00 EDT'))->getTimestamp(), (new \DateTime('1997-09-02 09:45:00 EDT'))->getTimestamp(), (new \DateTime('1997-09-02 10:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-09-02 10:15:00 EDT'))->getTimestamp(), ]; foreach ($records as $record) { $this->assertContainsEquals($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.'); } } /** * Every hour and a half for 4 occurrences: * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4 * ==> (September 2, 1997 EDT)09:00,10:30;12:00;13:30 */ public function test_every_90minutes_4_count(): void { global $DB; $rrule = 'FREQ=MINUTELY;INTERVAL=90;COUNT=4'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(4, $records); $expecteddates = [ (new \DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-09-02 10:30:00 EDT'))->getTimestamp(), (new \DateTime('1997-09-02 12:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-09-02 13:30:00 EDT'))->getTimestamp(), ]; foreach ($records as $record) { $this->assertContainsEquals($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.'); } } /** * Every 20 minutes from 9:00 AM to 4:40 PM every day for 100 times: * * (Original RFC example is set to everyday forever, but that will just take a lot of time for the test, * so just limit the count to 50). * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;COUNT=50 * ==> (September 2, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40 * (September 3, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40 * ... */ public function test_every_20minutes_daily_byhour_byminute_50_count(): void { global $DB; $rrule = 'FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;COUNT=50'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $byminuteinterval = new \DateInterval('PT20M'); $bydayinterval = new \DateInterval('P1D'); $date = new \DateTime('1997-09-02 09:00:00 EDT'); $expecteddates = []; $count = 50; for ($i = 0; $i < $count; $i++) { $expecteddates[] = $date->getTimestamp(); $date->add($byminuteinterval); if ($date->format('H') > 16) { // Go to next day. $date->add($bydayinterval); // Reset time to 9am. $date->setTime(9, 0); } } $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount($count, $records); foreach ($records as $record) { $this->assertContainsEquals($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.'); } } /** * Every 20 minutes from 9:00 AM to 4:40 PM every day for 100 times: * * (Original RFC example is set to everyday forever, but that will just take a lot of time for the test, * so just limit the count to 50). * * DTSTART;TZID=US-Eastern:19970902T090000 * RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;COUNT=50 * ==> (September 2, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40 * (September 3, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40 * ... */ public function test_every_20minutes_minutely_byhour_50_count(): void { global $DB; $rrule = 'FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;COUNT=50'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $byminuteinterval = new \DateInterval('PT20M'); $bydayinterval = new \DateInterval('P1D'); $date = new \DateTime('1997-09-02 09:00:00'); $expecteddates = []; $count = 50; for ($i = 0; $i < $count; $i++) { $expecteddates[] = $date->getTimestamp(); $date->add($byminuteinterval); if ($date->format('H') > 16) { // Go to next day. $date->add($bydayinterval); // Reset time to 9am. $date->setTime(9, 0); } } $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount($count, $records); foreach ($records as $record) { $this->assertContainsEquals($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.'); } } /** * An example where the days generated makes a difference because of WKST: * * DTSTART;TZID=US-Eastern:19970805T090000 * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO * ==> (1997 EDT)Aug 5,10,19,24 */ public function test_weekly_byday_with_wkst_mo(): void { global $DB; $this->change_event_startdate('19970805T090000', 'America/New_York'); $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(4, $records); $expecteddates = [ (new \DateTime('1997-08-05 09:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-08-10 09:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-08-19 09:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-08-24 09:00:00 EDT'))->getTimestamp(), ]; foreach ($records as $record) { $this->assertContainsEquals($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.'); } } /** * An example where the days generated makes a difference because of WKST: * Changing only WKST from MO to SU, yields different results... * * DTSTART;TZID=US-Eastern:19970805T090000 * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU * ==> (1997 EDT)August 5,17,19,31 */ public function test_weekly_byday_with_wkst_su(): void { global $DB; $this->change_event_startdate('19970805T090000', 'America/New_York'); $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(4, $records); $expecteddates = [ (new \DateTime('1997-08-05 09:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-08-17 09:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-08-19 09:00:00 EDT'))->getTimestamp(), (new \DateTime('1997-08-31 09:00:00 EDT'))->getTimestamp(), ]; foreach ($records as $record) { $this->assertContainsEquals($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.'); } } /* * Other edge case tests. */ /** * Tests for MONTHLY RRULE with BYMONTHDAY set to 31. * Should not include February, April, June, September and November. */ public function test_monthly_bymonthday_31(): void { global $DB; $rrule = 'FREQ=MONTHLY;BYMONTHDAY=31;COUNT=20'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(20, $records); $non31months = ['February', 'April', 'June', 'September', 'November']; foreach ($records as $record) { $month = date('F', $record->timestart); $this->assertNotContains($month, $non31months); } } /** * Tests for the last day in February. (Leap year test) */ public function test_yearly_on_the_last_day_of_february(): void { global $DB; $rrule = 'FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1;COUNT=30'; $mang = new rrule_manager($rrule); $mang->parse_rrule(); $mang->create_events($this->event); $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart'); $this->assertCount(30, $records); foreach ($records as $record) { $date = new \DateTime(date('Y-m-d H:i:s', $record->timestart)); $year = $date->format('Y'); $day = $date->format('d'); if ($year % 4 == 0) { $this->assertEquals(29, $day); } else { $this->assertEquals(28, $day); } } } /** * Change the event's timestart (DTSTART) based on the test's needs. * * @param string $datestr The date string. In 'Ymd\This' format. e.g. 19990902T090000. * @param null|string $timezonestr A valid timezone string. e.g. 'America/New_York'. * If not provided, the default timezone will be used. * @return bool|DateTime */ protected function change_event_startdate($datestr, $timezonestr = null) { // Use default timezone if not provided. if ($timezonestr === null) { $newdatetime = \DateTime::createFromFormat('Ymd\THis', $datestr); } else { $timezone = new \DateTimeZone($timezonestr); $newdatetime = \DateTime::createFromFormat('Ymd\THis', $datestr, $timezone); } // Update the start date of the parent event. $calevent = \calendar_event::load($this->event->id); $updatedata = (object)[ 'timestart' => $newdatetime->getTimestamp(), 'repeatid' => $this->event->id ]; $calevent->update($updatedata, false); $this->event->timestart = $calevent->timestart; return $newdatetime; } } PK*\-wT!T!tests/helpers.phpnu[. /** * This file contains helper classes and functions for testing. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); use core_calendar\local\event\entities\action_event; use core_calendar\local\event\entities\event; use core_calendar\local\event\entities\repeat_event_collection; use core_calendar\local\event\proxies\std_proxy; use core_calendar\local\event\proxies\coursecat_proxy; use core_calendar\local\event\proxies\cm_info_proxy; use core_calendar\local\event\value_objects\action; use core_calendar\local\event\value_objects\event_description; use core_calendar\local\event\value_objects\event_times; use core_calendar\local\event\factories\event_factory_interface; /** * Create a calendar event with the given properties. * * @param array $properties The properties to set on the event * @return \calendar_event */ function create_event($properties) { $record = new \stdClass(); $record->name = 'event name'; $record->eventtype = 'site'; $record->repeat = 0; $record->repeats = 0; $record->timestart = time(); $record->timeduration = 0; $record->timesort = 0; $record->type = CALENDAR_EVENT_TYPE_STANDARD; $record->courseid = 0; $record->categoryid = 0; foreach ($properties as $name => $value) { $record->$name = $value; } $event = new \calendar_event($record); return $event->create($record); } /** * Helper function to create a x number of events for each event type. * * @param int $quantity The quantity of events to be created. * @return array List of created events. */ function create_standard_events(int $quantity): array { $types = ['site', 'category', 'course', 'group', 'user']; $events = []; foreach ($types as $eventtype) { // Create five events of each event type. for ($i = 0; $i < $quantity; $i++) { $events[] = create_event(['eventtype' => $eventtype]); } } return $events; } /** * Helper function to create an action event. * * @param array $data The event data. * @return bool|calendar_event */ function create_action_event(array $data) { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); if (!isset($data['modulename']) || !isset($data['instance'])) { throw new coding_exception('Module and instance should be specified when creating an action event.'); } $isuseroverride = isset($data->priority) && $data->priority == CALENDAR_EVENT_USER_OVERRIDE_PRIORITY; if ($isuseroverride) { if (!in_array($data['modulename'], ['assign', 'lesson', 'quiz'])) { throw new coding_exception('Only assign, lesson and quiz modules supports overrides'); } } $event = array_merge($data, [ 'eventtype' => isset($data['eventtype']) ? $data['eventtype'] : 'open', 'courseid' => isset($data['courseid']) ? $data['courseid'] : 0, 'instance' => $data['instance'], 'modulename' => $data['modulename'], 'type' => CALENDAR_EVENT_TYPE_ACTION, ]); return create_event($event); } /** * Helper function to create an user override calendar event. * * @param string $modulename The modulename. * @param int $instanceid The instance id. * @param int $userid The user id. * @return calendar_event|false */ function create_user_override_event(string $modulename, int $instanceid, int $userid) { if (!isset($userid)) { throw new coding_exception('Must specify userid when creating a user override.'); } return create_action_event([ 'modulename' => $modulename, 'instance' => $instanceid, 'userid' => $userid, 'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY, ]); } /** * Helper function to create an group override calendar event. * * @param string $modulename The modulename. * @param int $instanceid The instance id. * @param int $courseid The course id. * @param int $groupid The group id. * @return calendar_event|false */ function create_group_override_event(string $modulename, int $instanceid, int $courseid, int $groupid) { if (!isset($groupid)) { throw new coding_exception('Must specify groupid when creating a group override.'); } return create_action_event([ 'groupid' => $groupid, 'courseid' => $courseid, 'modulename' => $modulename, 'instance' => $instanceid, ]); } /** * A test factory that will create action events. * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late */ class action_event_test_factory implements event_factory_interface { /** * @var callable $callback. */ private $callback; /** * A test factory that will create action events. The factory accepts a callback * that will be used to determine if the event should be returned or not. * * The callback will be given the event and should return true if the event * should be returned and false otherwise. * * @param callable $callback The callback. */ public function __construct($callback = null) { $this->callback = $callback; } public function create_instance(\stdClass $record) { $module = null; $subscription = null; if ($record->instance && $record->modulename) { $module = new cm_info_proxy($record->instance, $record->modulename, $record->courseid); } if ($record->subscriptionid) { $subscription = new std_proxy($record->subscriptionid, function($id) { return (object)['id' => $id]; }); } $event = new event( $record->id, $record->name, new event_description($record->description, $record->format), new coursecat_proxy($record->categoryid), new std_proxy($record->courseid, function($id) { $course = new \stdClass(); $course->id = $id; return $course; }), new std_proxy($record->groupid, function($id) { $group = new \stdClass(); $group->id = $id; return $group; }), new std_proxy($record->userid, function($id) { $user = new \stdClass(); $user->id = $id; return $user; }), !empty($record->repeatid) ? new repeat_event_collection($record, $this) : null, $module, $record->eventtype, new event_times( (new \DateTimeImmutable())->setTimestamp($record->timestart), (new \DateTimeImmutable())->setTimestamp($record->timestart + $record->timeduration), (new \DateTimeImmutable())->setTimestamp($record->timesort ? $record->timesort : $record->timestart), (new \DateTimeImmutable())->setTimestamp($record->timemodified), (new \DateTimeImmutable())->setTimestamp($record->timesort ? usergetmidnight($record->timesort) : 0) ), !empty($record->visible), $subscription, $record->location, !empty($record->component) ? $record->component : null ); $action = new action( 'Test action', new \moodle_url('/'), 1, true ); $actionevent = new action_event($event, $action); if ($callback = $this->callback) { return $callback($actionevent) ? $actionevent : false; } else { return $actionevent; } } } PK*\G^^tests/fixtures/import.icsnu[BEGIN:VCALENDAR METHOD:PUBLISH PRODID:-//John Papaioannou/NONSGML Bennu 0.1//EN VERSION:2.0 BEGIN:VEVENT UID:198001@127.0.0.1:8888/moodle32 SUMMARY:Event on 2-15-2017 DESCRIPTION:Event on 2-15-2017 CLASS:PUBLIC LAST-MODIFIED:20170226T014326Z LOCATION:Some place DTSTAMP:20170226T014355Z DTSTART;VALUE=DATE:20170214 DTEND;VALUE=DATE:20170215 END:VEVENT BEGIN:VEVENT UID:198000@127.0.0.1:8888/moodle32 SUMMARY:Event on 2-25-2017 DESCRIPTION:Event on 2-25-2017 CLASS:PUBLIC LAST-MODIFIED:20170226T014258Z DTSTAMP:20170226T014355Z DTSTART;VALUE=DATE:20170224 DTEND;VALUE=DATE:20170225 END:VEVENT END:VCALENDAR PK*\}ww)tests/fixtures/import_with_parameters.icsnu[BEGIN:VCALENDAR METHOD:PUBLISH PRODID:-//Huong Nguyen/NONSGML Moodle//EN VERSION:2.0 BEGIN:VEVENT UID:special_import_moodle_1 SUMMARY:First event DESCRIPTION:Description of the first event\n CLASS:PUBLIC LAST-MODIFIED:20220728T034015Z LOCATION:Vietnam DTSTAMP:20220728T034031Z DTSTART:20220701T070000Z DTEND:20220701T070000Z END:VEVENT BEGIN:VEVENT UID:special_import_moodle_2 SUMMARY:Second event DESCRIPTION;ALTREP="http://moodle.com/":Description of the second event\n CLASS:PUBLIC LAST-MODIFIED:20220728T034021Z LOCATION:Vietnam DTSTAMP:20220728T034031Z DTSTART:20220701T080000Z DTEND:20220701T080000Z END:VEVENT END:VCALENDAR PK*\ tests/event_description_test.phpnu[. namespace core_calendar; use core_calendar\local\event\value_objects\event_description; /** * Action testcase. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class event_description_test extends \advanced_testcase { /** * Test event description class getters. * * @dataProvider getters_testcases * @param array $constructorparams Associative array of constructor parameters. */ public function test_getters($constructorparams): void { $eventdescription = new event_description( $constructorparams['value'], $constructorparams['format'] ); foreach ($constructorparams as $name => $value) { $this->assertEquals($eventdescription->{'get_' . $name}(), $value); } } /** * Test cases for getters test. */ public static function getters_testcases(): array { return [ 'Dataset 1' => [ 'constructorparams' => [ 'value' => 'Hello', 'format' => 1 ] ], 'Dataset 2' => [ 'constructorparams' => [ 'value' => 'Goodbye', 'format' => 2 ] ] ]; } } PK*\~tests/event_test.phpnu[. namespace core_calendar; use core_calendar\local\event\entities\event; use core_calendar\local\event\proxies\std_proxy; use core_calendar\local\event\proxies\coursecat_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_collection_interface; defined('MOODLE_INTERNAL') || die(); /** * Calendar event tests.. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class event_test extends \advanced_testcase { /** * Test event class getters. * * @dataProvider getters_testcases * @param array $constructorparams Associative array of constructor parameters. */ public function test_getters($constructorparams): void { $event = new event( $constructorparams['id'], $constructorparams['name'], $constructorparams['description'], $constructorparams['category'], $constructorparams['course'], $constructorparams['group'], $constructorparams['user'], $constructorparams['repeats'], $constructorparams['course_module'], $constructorparams['type'], $constructorparams['times'], $constructorparams['visible'], $constructorparams['subscription'], $constructorparams['location'], $constructorparams['component'] ); foreach ($constructorparams as $name => $value) { if ($name !== 'visible' && $name !== 'component') { $this->assertEquals($event->{'get_' . $name}(), $value); } } $this->assertEquals($event->is_visible(), $constructorparams['visible']); $this->assertEquals('mod_' . $event->get_course_module()->get('modname'), $event->get_component()); } /** * Test cases for getters test. */ public static function getters_testcases(): array { $lamecallable = function($id) { return (object)['id' => $id, 'modname' => 'assign']; }; return [ 'Dataset 1' => [ 'constructorparams' => [ 'id' => 1, 'name' => 'Test event 1', 'description' => new event_description('asdf', 1), 'category' => new coursecat_proxy(0), 'course' => new std_proxy(1, $lamecallable), 'group' => new std_proxy(1, $lamecallable), 'user' => new std_proxy(1, $lamecallable), 'repeats' => new core_calendar_event_test_event_collection(), 'course_module' => new std_proxy(1, $lamecallable), 'type' => 'dunno what this actually is meant to be', 'times' => new event_times( (new \DateTimeImmutable())->setTimestamp(-386380800), (new \DateTimeImmutable())->setTimestamp(115776000), (new \DateTimeImmutable())->setTimestamp(115776000), (new \DateTimeImmutable())->setTimestamp(time()), (new \DateTimeImmutable())->setTimestamp(115776000) ), 'visible' => true, 'subscription' => new std_proxy(1, $lamecallable), 'location' => 'Test', 'component' => null ] ], ]; } } /** * Test event class. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_calendar_event_test_event_collection implements event_collection_interface { /** * @var array $events Array of events. */ protected $events; /** * Constructor. */ public function __construct() { $this->events = [ 'not really an event hahaha', 'also not really. gottem.' ]; } public function get_id() { return 1729; } public function get_num() { return 2; } public function getIterator(): \Traversable { foreach ($this->events as $event) { yield $event; } } } PK*\ HHtests/privacy/provider_test.phpnu[. /** * Privacy tests for core_calendar. * * @package core_calendar * @category test * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); require_once($CFG->dirroot . '/calendar/tests/externallib_test.php'); use core_calendar\privacy\provider; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\writer; use core_privacy\tests\provider_testcase; use core_privacy\local\request\approved_userlist; /** * Unit tests for calendar/classes/privacy/provider * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_calendar\privacy\provider */ final class provider_test extends provider_testcase { /** * Overriding setUp() function to always reset after tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Test for provider::get_contexts_for_userid(). * * @throws coding_exception */ public function test_get_contexts_for_userid(): void { // Create test user to create Calendar Events and Subscriptions. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Create a Category and Courses to assign Calendar Events and Subscriptions. $category = $this->getDataGenerator()->create_category(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $grouprecord = (object)[ 'courseid' => $course3->id, 'name' => 'test_group' ]; $course3group = $this->getDataGenerator()->create_group($grouprecord); // Get contexts. $usercontext = \context_user::instance($user->id); $categorycontext = \context_coursecat::instance($category->id); $course1context = \context_course::instance($course1->id); $course2context = \context_course::instance($course2->id); $course3context = \context_course::instance($course3->id); // Add Category Calendar Events for Category. $this->create_test_standard_calendar_event('category', $user->id, time(), '', $category->id); $this->create_test_standard_calendar_event('category', $user->id, time(), '', $category->id); // Add User Calendar Events for User. $this->create_test_standard_calendar_event('user', $user->id, time(), ''); $this->create_test_standard_calendar_event('user', $user->id, time(), '', 0, $course1->id); $this->create_test_standard_calendar_event('user', $user->id, time(), '', 0, $course2->id); // Add a Course Calendar Event for Course 1. $this->create_test_standard_calendar_event('course', $user->id, time(), '', 0, $course1->id); // Add a Course Assignment Action Calendar Event for Course 2. $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $params['course'] = $course2->id; $params['assignsubmission_onlinetext_enabled'] = 1; $instance = $generator->create_instance($params); $cm = get_coursemodule_from_instance('assign', $instance->id); $modulecontext = \context_module::instance($cm->id); $assign = new \assign($modulecontext, $cm, $course2); $this->create_test_action_calendar_event('duedate', $course2->id, $instance->id, 'assign', $user->id, time()); $this->create_test_action_calendar_event('gradingduedate', $course2->id, $instance->id, 'assign', $user->id, time()); // Add a Calendar Subscription and Group Calendar Event to Course 3. $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user->id, 0, $course3->id); $this->create_test_standard_calendar_event('group', $user->id, time(), '', 0, $course3->id, $course3group->id); // The user will be in these contexts. $usercontextids = [ $usercontext->id, $categorycontext->id, $course1context->id, $modulecontext->id, $course3context->id ]; // Retrieve the user's context ids. $contextids = provider::get_contexts_for_userid($user->id); // Check the user context list and retrieved user context lists contains the same number of records. $this->assertEquals(count($usercontextids), count($contextids->get_contextids())); // There should be no difference between the contexts. $this->assertEmpty(array_diff($usercontextids, $contextids->get_contextids())); } /** * Test for provider::export_user_data(). * * @throws coding_exception */ public function test_export_user_data(): void { global $DB; // Create test user to create Calendar Events and Subscriptions with. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Create a Category to test creating a Category Calendar Event. $category = $this->getDataGenerator()->create_category(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $grouprecord = (object)[ 'courseid' => $course3->id, 'name' => 'test_group' ]; $course3group = $this->getDataGenerator()->create_group($grouprecord); // Add User Calendar Events for User. $event1 = $this->create_test_standard_calendar_event('user', $user->id, time(), ''); // Add Category Calendar Events for Category. $event2 = $this->create_test_standard_calendar_event('category', $user->id, time(), '', $category->id); // Add two Course Calendar Event for Course 1 and set the same time (1 day a head). $time = strtotime('+1 day', time()); $event3 = $this->create_test_standard_calendar_event('course', $user->id, $time, 'ABC', 0, $course1->id); $event4 = $this->create_test_standard_calendar_event('course', $user->id, $time, 'DEF', 0, $course1->id); // Add a Course Assignment Action Calendar Event for Course 2. $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $params['course'] = $course2->id; $params['assignsubmission_onlinetext_enabled'] = 1; $instance = $generator->create_instance($params); $cm = get_coursemodule_from_instance('assign', $instance->id); $modulecontext = \context_module::instance($cm->id); $assign = new \assign($modulecontext, $cm, $course2); $event5 = $this->create_test_action_calendar_event('duedate', $course2->id, $instance->id, 'assign', $user->id, time()); // Add a Calendar Subscription and Group Calendar Event to Course 3. $subscription1 = $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user->id, 0, $course3->id); $event6 = $this->create_test_standard_calendar_event('group', $user->id, time(), '', 0, $course3->id, $course3group->id); // Retrieve the user's context ids. $contextlist = provider::get_contexts_for_userid($user->id); $approvedcontextlist = new approved_contextlist($user, 'core_calendar', $contextlist->get_contextids()); // Retrieve Calendar Event and Subscriptions data only for this user. provider::export_user_data($approvedcontextlist); foreach ($contextlist as $context) { /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); // Test event 1 that was created for the test User. if ($context->instanceid == $user->id && $context->contextlevel == CONTEXT_USER) { // Test the content contains Calendar Event user data. $subcontexts = [ get_string('calendar', 'calendar'), get_string('events', 'calendar'), date('c', $event1->timestart) ]; $name = "user-event"; $data = $writer->get_related_data($subcontexts, $name); $this->assertEquals('Standard Calendar Event user', $data->name); } // Test event 2 that was created for the test Category. if ($context->instanceid == $category->id && $context->contextlevel == CONTEXT_COURSECAT) { // Test the content contains Calendar Event category data. $subcontexts = [ get_string('calendar', 'calendar'), get_string('events', 'calendar'), date('c', $event2->timestart) ]; $name = "category-event"; $data = $writer->get_related_data($subcontexts, $name); $this->assertEquals('Standard Calendar Event category', $data->name); } // Test events 3, 4, and 5 that were created for the test Course 1. if ($context->instanceid == $course1->id && $context->contextlevel == CONTEXT_COURSE) { // Test the content contains Calendar Event course data set with the same time, and the exported files are uniquely identified. $subcontext1 = [ get_string('calendar', 'calendar'), get_string('events', 'calendar'), date('c', $event3->timestart) ]; $name1 = "course-event-1"; $data1 = $writer->get_related_data($subcontext1, $name1); $this->assertEquals('Standard Calendar Event course -- ABC', $data1->name); $subcontext2 = [ get_string('calendar', 'calendar'), get_string('events', 'calendar'), date('c', $event4->timestart) ]; $name2 = "course-event-2"; $data2 = $writer->get_related_data($subcontext2, $name2); $this->assertEquals('Standard Calendar Event course -- DEF', $data2->name); } // Test action event that were created for the test Course 2. if ($context->instanceid == $cm->id && $context->contextlevel == CONTEXT_MODULE) { // Test the content contains Calendar Action Event course data. $subcontexts = [ get_string('calendar', 'calendar'), get_string('events', 'calendar'), date('c', $event5->timestart) ]; $name = "duedate-event"; $data = $writer->get_related_data($subcontexts, $name); $this->assertEquals('Action Calendar Event duedate -- assign', $data->name); } // Test Calendar Subscription and Event that were created for the test Course 3. if ($context->instanceid == $course3->id && $context->contextlevel == CONTEXT_COURSE) { // Test the content contains Calendar Subscription data also created for the test Course 3. $subcontexts = [ get_string('calendar', 'calendar'), get_string('subscriptions', 'calendar') ]; $name = "course-subscription"; $data = $writer->get_related_data($subcontexts, $name); $this->assertEquals('Calendar Subscription course', $data->name); // Test the content contains Calendar Event group data also created for the test Course 3. $subcontexts = [ get_string('calendar', 'calendar'), get_string('events', 'calendar'), date('c', $event6->timestart) ]; $name = "group-event"; $data = $writer->get_related_data($subcontexts, $name); $this->assertEquals('Standard Calendar Event group', $data->name); } } } /** * Test for provider::test_export_user_preferences(). */ public function test_export_user_preferences(): void { global $DB; // Test setup. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Add a user home page preference for the User. set_user_preference('calendar_savedflt', 'true', $user); // Test the user preference exists. $params = [ 'userid' => $user->id, 'name' => 'calendar_savedflt' ]; // Test the user preferences export contains 1 user preference record for the User. provider::export_user_preferences($user->id); $contextuser = \context_user::instance($user->id); $writer = writer::with_context($contextuser); $this->assertTrue($writer->has_any_data()); $exportedpreferences = $writer->get_user_preferences('core_calendar'); $this->assertCount(1, (array) $exportedpreferences); $this->assertEquals('true', $exportedpreferences->calendarsavedflt->value); } /** * Test for provider::delete_data_for_all_users_in_context(). * * @throws dml_exception */ public function test_delete_data_for_all_users_in_context(): void { global $DB; // Create test user to create Calendar Events and Subscriptions with. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Create a Course to test creating a Category Calendar Event. $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); // Get contexts. $course1context = \context_course::instance($course1->id); $course2context = \context_course::instance($course2->id); // Add a Course Calendar Event by User 1 for Course 1 and Course 2. $this->setUser($user1); $this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course1->id); $this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course2->id); // Add a Calendar Subscription by User 1 for Course 1. $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user1->id, 0, $course1->id); // Add a Course Calendar Event by User 2 for Course 1 and Course 2. $this->setUser($user2); $this->create_test_standard_calendar_event('course', $user2->id, time(), '', 0, $course1->id); $this->create_test_standard_calendar_event('course', $user2->id, time(), '', 0, $course2->id); // Add a Calendar Subscription by User 2 for Course 2. $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user2->id, 0, $course2->id); // Add a Course Assignment Action Calendar Event by User 2 for Course 2. $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $params['course'] = $course2->id; $params['assignsubmission_onlinetext_enabled'] = 1; $instance = $generator->create_instance($params); $cm = get_coursemodule_from_instance('assign', $instance->id); $modulecontext = \context_module::instance($cm->id); $assign = new \assign($modulecontext, $cm, $course2); $this->create_test_action_calendar_event('duedate', $course2->id, $instance->id, 'assign', $user2->id, time()); $this->create_test_action_calendar_event('gradingduedate', $course2->id, $instance->id, 'assign', $user2->id, time()); // Delete all Calendar Events for all Users by Context for Course 1. provider::delete_data_for_all_users_in_context($course1context); // Verify all Calendar Events for Course 1 were deleted. $events = $DB->get_records('event', array('courseid' => $course1->id)); $this->assertCount(0, $events); // Verify all Calendar Subscriptions for Course 1 were deleted. $subscriptions = $DB->get_records('event_subscriptions', array('courseid' => $course1->id)); $this->assertCount(0, $subscriptions); // Verify all Calendar Events for Course 2 exists still. $events = $DB->get_records('event', array('courseid' => $course2->id)); $this->assertCount(4, $events); // Verify all Calendar Subscriptions for Course 2 exists still. $subscriptions = $DB->get_records('event_subscriptions', array('courseid' => $course2->id)); $this->assertCount(1, $subscriptions); // Delete all Calendar Events for all Users by Context for Course 2. provider::delete_data_for_all_users_in_context($course2context); // Verify all Calendar Events for Course 2 context were deleted. $events = $DB->get_records('event', array('courseid' => $course2->id, 'modulename' => '0')); $this->assertCount(0, $events); // Verify all Calendar Subscriptions for Course 2 were deleted. $subscriptions = $DB->get_records('event_subscriptions', array('courseid' => $course2->id)); $this->assertCount(0, $subscriptions); // Verify all Calendar Events for the assignment exists still. $events = $DB->get_records('event', array('modulename' => 'assign')); $this->assertCount(2, $events); // Delete all Calendar Events for all Users by Context for the assignment. provider::delete_data_for_all_users_in_context($modulecontext); // Verify all Calendar Events for the assignment context were deleted. $events = $DB->get_records('event', array('modulename' => 'assign')); $this->assertCount(0, $events); } /** * Test for provider::delete_data_for_user(). * * @throws dml_exception */ public function test_delete_data_for_user(): void { global $DB; // Create test user to create Calendar Events and Subscriptions with. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Create a Category and Courses to test creating a Category Calendar Event. $category = $this->getDataGenerator()->create_category(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); // Add 5 Calendar Events for User 1 for various contexts. $this->setUser($user1); $this->create_test_standard_calendar_event('user', $user1->id, time(), ''); $this->create_test_standard_calendar_event('site', $user1->id, time(), '', 0, 1); $this->create_test_standard_calendar_event('category', $user1->id, time(), '', $category->id); $this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course1->id); $this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course2->id); // Add 1 Calendar Subscription for User 1 at course context. $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user1->id, 0, $course2->id); // Add 3 Calendar Events for User 2 for various contexts. $this->setUser($user2); $this->create_test_standard_calendar_event('user', $user2->id, time(), ''); $this->create_test_standard_calendar_event('category', $user2->id, time(), '', $category->id); $this->create_test_standard_calendar_event('course', $user2->id, time(), '', 0, $course1->id); // Add 1 Calendar Subscription for User 2 at course context. $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user2->id, 0, $course2->id); // Retrieve the user's context ids. $contextlist = provider::get_contexts_for_userid($user1->id); $approvedcontextlist = new approved_contextlist($user1, 'core_calendar', $contextlist->get_contextids()); // Delete all Calendar data for User 1. provider::delete_data_for_user($approvedcontextlist); // Test all Calendar Events and Subscriptions for User 1 equals zero. $events = $DB->get_records('event', ['userid' => $user1->id]); $this->assertCount(0, $events); $eventsubscriptions = $DB->get_records('event_subscriptions', ['userid' => $user1->id]); $this->assertCount(0, $eventsubscriptions); // Test all Calendar Events and Subscriptions for User 2 still exists and matches the same number created. $events = $DB->get_records('event', ['userid' => $user2->id]); $this->assertCount(3, $events); $eventsubscriptions = $DB->get_records('event_subscriptions', ['userid' => $user2->id]); $this->assertCount(1, $eventsubscriptions); } /** * Test that only users with a user context are fetched. */ public function test_get_users_in_context(): void { $component = 'core_calendar'; // Create user1 to create Calendar Events and Subscriptions. $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); // Create user2 to create Calendar Events and Subscriptions. $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); // Create user3 to create Calendar Events and Subscriptions. $user3 = $this->getDataGenerator()->create_user(); $usercontext3 = \context_user::instance($user3->id); // Create a Category and Courses to assign Calendar Events and Subscriptions. $category = $this->getDataGenerator()->create_category(); $categorycontext = \context_coursecat::instance($category->id); $course1 = $this->getDataGenerator()->create_course(); $course1context = \context_course::instance($course1->id); $course2 = $this->getDataGenerator()->create_course(); $course2context = \context_course::instance($course2->id); $course3 = $this->getDataGenerator()->create_course(); $course3context = \context_course::instance($course3->id); $grouprecord = (object)[ 'courseid' => $course3->id, 'name' => 'test_group' ]; $course3group = $this->getDataGenerator()->create_group($grouprecord); // Add Category Calendar Events for Category. $this->setUser($user1); $this->create_test_standard_calendar_event('category', $user1->id, time(), '', $category->id); $this->setUser($user2); $this->create_test_standard_calendar_event('category', $user2->id, time(), '', $category->id); // Add User Calendar Events for user1 and user2. $this->setUser($user1); $this->create_test_standard_calendar_event('user', $user1->id, time(), ''); $this->create_test_standard_calendar_event('user', $user1->id, time(), '', 0, $course1->id); $this->create_test_standard_calendar_event('user', $user1->id, time(), '', 0, $course2->id); $this->setUser($user2); $this->create_test_standard_calendar_event('user', $user2->id, time(), '', 0, $course1->id); // Add a Course Calendar Events for Course 1. $this->setUser($user1); $this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course1->id); $this->setUser($user2); $this->create_test_standard_calendar_event('course', $user2->id, time(), '', 0, $course1->id); // Add a Course Assignment Action Calendar Event for Course 2. $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $params['course'] = $course2->id; $params['assignsubmission_onlinetext_enabled'] = 1; $instance = $generator->create_instance($params); $cm = get_coursemodule_from_instance('assign', $instance->id); $modulecontext = \context_module::instance($cm->id); $assign = new \assign($modulecontext, $cm, $course2); $this->setUser($user2); $this->create_test_action_calendar_event('duedate', $course2->id, $instance->id, 'assign', $user2->id, time()); $this->create_test_action_calendar_event('gradingduedate', $course2->id, $instance->id, 'assign', $user2->id, time()); // Add a Calendar Subscription and Group Calendar Event to Course 3. $this->create_test_standard_calendar_event('group', $user2->id, time(), '', 0, $course3->id, $course3group->id); $this->setUser($user3); $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user3->id, 0, $course3->id); // The user list for usercontext1 should return user1. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $this->assertTrue(in_array($user1->id, $userlist1->get_userids())); // The user list for usercontext2 should return user2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $this->assertTrue(in_array($user2->id, $userlist2->get_userids())); // The user list for course1context should return user1 and user2. $userlist3 = new \core_privacy\local\request\userlist($course1context, $component); provider::get_users_in_context($userlist3); $this->assertCount(2, $userlist3); $this->assertTrue(in_array($user1->id, $userlist3->get_userids())); $this->assertTrue(in_array($user2->id, $userlist3->get_userids())); // The user list for course2context should not return any users. $userlist4 = new \core_privacy\local\request\userlist($course2context, $component); provider::get_users_in_context($userlist4); $this->assertCount(0, $userlist4); // The user list for course3context should return user2 and user3. $userlist5 = new \core_privacy\local\request\userlist($course3context, $component); provider::get_users_in_context($userlist5); $this->assertCount(2, $userlist5); $this->assertTrue(in_array($user2->id, $userlist5->get_userids())); $this->assertTrue(in_array($user3->id, $userlist5->get_userids())); // The user list for categorycontext should return user1 and user2. $userlist6 = new \core_privacy\local\request\userlist($categorycontext, $component); provider::get_users_in_context($userlist6); $this->assertCount(2, $userlist6); $this->assertTrue(in_array($user1->id, $userlist6->get_userids())); $this->assertTrue(in_array($user2->id, $userlist6->get_userids())); // The user list for modulecontext should return user2. $userlist7 = new \core_privacy\local\request\userlist($modulecontext, $component); provider::get_users_in_context($userlist7); $this->assertCount(1, $userlist7); $this->assertTrue(in_array($user2->id, $userlist7->get_userids())); // The user list for usercontext3 should not return any users. $userlist8 = new \core_privacy\local\request\userlist($usercontext3, $component); provider::get_users_in_context($userlist8); $this->assertCount(0, $userlist8); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users(): void { $component = 'core_calendar'; // Create user1 to create Calendar Events and Subscriptions. $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); // Create user2 to create Calendar Events and Subscriptions. $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); // Create user3 to create Calendar Events and Subscriptions. $user3 = $this->getDataGenerator()->create_user(); $usercontext3 = \context_user::instance($user3->id); // Create a Category and Courses to assign Calendar Events and Subscriptions. $category = $this->getDataGenerator()->create_category(); $categorycontext = \context_coursecat::instance($category->id); $course1 = $this->getDataGenerator()->create_course(); $course1context = \context_course::instance($course1->id); $course2 = $this->getDataGenerator()->create_course(); $course2context = \context_course::instance($course2->id); $course3 = $this->getDataGenerator()->create_course(); $course3context = \context_course::instance($course3->id); $grouprecord = (object)[ 'courseid' => $course3->id, 'name' => 'test_group' ]; $course3group = $this->getDataGenerator()->create_group($grouprecord); // Add Category Calendar Events for Category. $this->setUser($user1); $this->create_test_standard_calendar_event('category', $user1->id, time(), '', $category->id); $this->setUser($user2); $this->create_test_standard_calendar_event('category', $user2->id, time(), '', $category->id); // Add User Calendar Events for user1 and user2. $this->setUser($user1); $this->create_test_standard_calendar_event('user', $user1->id, time(), ''); $this->create_test_standard_calendar_event('user', $user1->id, time(), '', 0, $course1->id); $this->create_test_standard_calendar_event('user', $user1->id, time(), '', 0, $course2->id); $this->setUser($user2); $this->create_test_standard_calendar_event('user', $user2->id, time(), '', 0, $course1->id); // Add a Course Calendar Events for Course 1. $this->setUser($user1); $this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course1->id); $this->setUser($user2); $this->create_test_standard_calendar_event('course', $user2->id, time(), '', 0, $course1->id); // Add a Course Assignment Action Calendar Event for Course 2. $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $params['course'] = $course2->id; $params['assignsubmission_onlinetext_enabled'] = 1; $instance = $generator->create_instance($params); $cm = get_coursemodule_from_instance('assign', $instance->id); $modulecontext = \context_module::instance($cm->id); $assign = new \assign($modulecontext, $cm, $course2); $this->setUser($user2); $this->create_test_action_calendar_event('duedate', $course2->id, $instance->id, 'assign', $user2->id, time()); $this->create_test_action_calendar_event('gradingduedate', $course2->id, $instance->id, 'assign', $user2->id, time()); // Add a Calendar Subscription and Group Calendar Event to Course 3. $this->create_test_standard_calendar_event('group', $user2->id, time(), '', 0, $course3->id, $course3group->id); $this->setUser($user3); $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user3->id, 0, $course3->id); // The user list for usercontext1 should return user1. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); // The user list for usercontext2 should return user2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // The user list for course1context should return user1 and user2. $userlist3 = new \core_privacy\local\request\userlist($course1context, $component); provider::get_users_in_context($userlist3); $this->assertCount(2, $userlist3); // The user list for course2context should not return any users. $userlist4 = new \core_privacy\local\request\userlist($course2context, $component); provider::get_users_in_context($userlist4); $this->assertCount(0, $userlist4); // The user list for course3context should return user2 and user3. $userlist5 = new \core_privacy\local\request\userlist($course3context, $component); provider::get_users_in_context($userlist5); $this->assertCount(2, $userlist5); // The user list for categorycontext should return user1 and user2. $userlist6 = new \core_privacy\local\request\userlist($categorycontext, $component); provider::get_users_in_context($userlist6); $this->assertCount(2, $userlist6); // The user list for modulecontext should return user2. $userlist7 = new \core_privacy\local\request\userlist($modulecontext, $component); provider::get_users_in_context($userlist7); $this->assertCount(1, $userlist7); // The user list for usercontext3 should not return any users. $userlist8 = new \core_privacy\local\request\userlist($usercontext3, $component); provider::get_users_in_context($userlist8); $this->assertCount(0, $userlist8); // Convert $userlist1 into an approved_contextlist. $approvedlist1 = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist1); // The user list for usercontext1 should not return any users. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // The user list for usercontext2 should still return users2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // Convert $userlist3 into an approved_contextlist. // Pass an empty array as a value for the approved user list. $approvedlist2 = new approved_userlist($course1context, $component, []); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist2); // The user list for course1context should return user1 and user2. $userlist3 = new \core_privacy\local\request\userlist($course1context, $component); provider::get_users_in_context($userlist3); $this->assertCount(2, $userlist3); $this->assertTrue(in_array($user1->id, $userlist3->get_userids())); $this->assertTrue(in_array($user2->id, $userlist3->get_userids())); // Convert $userlist3 into an approved_contextlist. // Pass the ID of user1 as a value for the approved user list. $approvedlist2 = new approved_userlist($course1context, $component, [$user1->id]); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist2); // The user list for course1context should return user2. $userlist3 = new \core_privacy\local\request\userlist($course1context, $component); provider::get_users_in_context($userlist3); $this->assertCount(1, $userlist3); $this->assertTrue(in_array($user2->id, $userlist3->get_userids())); // The user list for course3context should still return user2 and user3. $userlist5 = new \core_privacy\local\request\userlist($course3context, $component); provider::get_users_in_context($userlist5); $this->assertCount(2, $userlist5); // Convert $userlist6 into an approved_contextlist. $approvedlist3 = new approved_userlist($categorycontext, $component, $userlist6->get_userids()); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist3); // The user list for categorycontext should not return any users. $userlist6 = new \core_privacy\local\request\userlist($categorycontext, $component); provider::get_users_in_context($userlist6); $this->assertCount(0, $userlist6); // Convert $userlist7 into an approved_contextlist. $approvedlist4 = new approved_userlist($modulecontext, $component, $userlist7->get_userids()); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist4); // The user list for modulecontext should not return any users. $userlist7 = new \core_privacy\local\request\userlist($modulecontext, $component); provider::get_users_in_context($userlist7); $this->assertCount(0, $userlist7); } // Start of helper functions. /** * Helper function to create a Standard Calendar Event. * * @param string $eventtype Calendar event type * @param int $userid User Id * @param int $time Timestamp value * @param string $customname Custom name * @param int $categoryid Course Category Id * @param int $courseid Course Id * @param int $groupid Group Id * @return bool|calendar_event Standard Calendar Event created. * @throws coding_exception */ protected function create_test_standard_calendar_event($eventtype, $userid, $time, $customname = '', $categoryid = 0, $courseid = 0, $groupid = 0) { // Create a standard calendar event. $name = "Standard Calendar Event $eventtype"; if ($customname != '') { $name .= " -- $customname"; } $event = (object)[ 'name' => $name, 'categoryid' => $categoryid, 'courseid' => $courseid, 'groupid' => $groupid, 'userid' => $userid, 'modulename' => 0, 'instance' => 0, 'eventtype' => $eventtype, 'type' => CALENDAR_EVENT_TYPE_STANDARD, 'timestart' => $time, 'visible' => 1 ]; return \calendar_event::create($event, false); } /** * Helper function to create an Action Calendar Event. * * @param string $eventtype Calendar event type * @param int $courseid Course Id * @param int $instanceid Activity Module instance id * @param string $modulename Activity Module name * @param int $userid User Id * @param int $time Timestamp value * @return bool|calendar_event Action Calendar Event created. * @throws coding_exception */ protected function create_test_action_calendar_event($eventtype, $courseid, $instanceid, $modulename, $userid, $time) { // Create an action calendar event. $event = (object)[ 'name' => "Action Calendar Event $eventtype -- $modulename", 'categoryid' => 0, 'courseid' => $courseid, 'groupid' => 0, 'userid' => $userid, 'modulename' => $modulename, 'instance' => $instanceid, 'eventtype' => $eventtype, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => $time, 'visible' => 1 ]; return \calendar_event::create($event, false); } /** * Helper function to create a Calendar Subscription. * * @param string $eventtype Calendar Subscription event type * @param string $url Calendar Subscription URL * @param int $userid User Id * @param int $categoryid Category Id * @param int $courseid Course Id * @param int $groupid Group Id * @return int Calendar Subscription Id */ protected function create_test_calendar_subscription($eventtype, $url, $userid, $categoryid = 0, $courseid = 0, $groupid = 0) { // Create a subscription calendar event. $subscription = (object)[ 'name' => "Calendar Subscription " . $eventtype, 'url' => $url, 'categoryid' => $categoryid, 'courseid' => $courseid, 'groupid' => $groupid, 'userid' => $userid, 'eventtype' => $eventtype ]; return calendar_add_subscription($subscription); } } PK*\m^^tests/container_test.phpnu[. namespace core_calendar; use core_calendar\local\event\entities\action_event; use core_calendar\local\event\entities\event; use core_calendar\local\event\entities\event_interface; use core_calendar\local\event\factories\event_factory; use core_calendar\local\event\factories\event_factory_interface; use core_calendar\local\event\mappers\event_mapper; use core_calendar\local\event\mappers\event_mapper_interface; use core_completion\api; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); /** * Event container test.. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_calendar\local\event\container */ final class container_test extends \advanced_testcase { /** * Test setup. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); } /** * Test getting the event factory. */ public function test_get_event_factory(): void { $factory = \core_calendar\local\event\container::get_event_factory(); // Test that the container is returning the right type. $this->assertInstanceOf(event_factory_interface::class, $factory); // Test that the container is returning the right implementation. $this->assertInstanceOf(event_factory::class, $factory); // Test that getting the factory a second time returns the same instance. $factory2 = \core_calendar\local\event\container::get_event_factory(); $this->assertTrue($factory === $factory2); } /** * Test that the event factory correctly creates instances of events. * * @dataProvider get_event_factory_testcases * @param \stdClass $dbrow Row from the "database". */ public function test_event_factory_create_instance($dbrow): void { $legacyevent = $this->create_event($dbrow); $factory = \core_calendar\local\event\container::get_event_factory(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); // Set some of the fake dbrow properties to match real data in the DB // this is necessary as the factory hides things that modinfo doesn't // know about. $dbrow->id = $legacyevent->id; $dbrow->courseid = $course->id; $dbrow->instance = $moduleinstance->id; $dbrow->modulename = 'assign'; $event = $factory->create_instance($dbrow); // Test that the factory is returning the right type. $this->assertInstanceOf(event_interface::class, $event); // Test that the factory is returning the right implementation. $this->assertTrue($event instanceof event || $event instanceof action_event); // Test that the event created has the correct properties. $this->assertEquals($legacyevent->id, $event->get_id()); $this->assertEquals($dbrow->description, $event->get_description()->get_value()); $this->assertEquals($dbrow->format, $event->get_description()->get_format()); $this->assertEquals($dbrow->courseid, $event->get_course()->get('id')); $this->assertEquals($dbrow->location, $event->get_location()); if ($dbrow->groupid == 0) { $this->assertNull($event->get_group()); } else { $this->assertEquals($dbrow->groupid, $event->get_group()->get('id')); } $this->assertEquals($dbrow->userid, $event->get_user()->get('id')); $this->assertEquals(null, $event->get_repeats()); $this->assertEquals($dbrow->modulename, $event->get_course_module()->get('modname')); $this->assertEquals($dbrow->instance, $event->get_course_module()->get('instance')); $this->assertEquals($dbrow->timestart, $event->get_times()->get_start_time()->getTimestamp()); $this->assertEquals($dbrow->timemodified, $event->get_times()->get_modified_time()->getTimestamp()); $this->assertEquals($dbrow->timesort, $event->get_times()->get_sort_time()->getTimestamp()); if ($dbrow->visible == 1) { $this->assertTrue($event->is_visible()); } else { $this->assertFalse($event->is_visible()); } if (!$dbrow->subscriptionid) { $this->assertNull($event->get_subscription()); } else { $this->assertEquals($event->get_subscription()->get('id')); } } /** * Test that the event factory deals with invisible modules properly as admin. * * @dataProvider get_event_factory_testcases * @param \stdClass $dbrow Row from the "database". */ public function test_event_factory_when_module_visibility_is_toggled_as_admin($dbrow): void { $legacyevent = $this->create_event($dbrow); $factory = \core_calendar\local\event\container::get_event_factory(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $dbrow->id = $legacyevent->id; $dbrow->courseid = $course->id; $dbrow->instance = $moduleinstance->id; $dbrow->modulename = 'assign'; set_coursemodule_visible($moduleinstance->cmid, 0); $event = $factory->create_instance($dbrow); // Test that the factory is returning an event as the admin can see hidden course modules. $this->assertInstanceOf(event_interface::class, $event); } /** * Test that the event factory deals with invisible modules properly as a guest. * * @dataProvider get_event_factory_testcases * @param \stdClass $dbrow Row from the "database". */ public function test_event_factory_when_module_visibility_is_toggled_as_guest($dbrow): void { $legacyevent = $this->create_event($dbrow); $factory = \core_calendar\local\event\container::get_event_factory(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $dbrow->id = $legacyevent->id; $dbrow->courseid = $course->id; $dbrow->instance = $moduleinstance->id; $dbrow->modulename = 'assign'; set_coursemodule_visible($moduleinstance->cmid, 0); // Set to a user who can not view hidden course modules. $this->setGuestUser(); $event = $factory->create_instance($dbrow); // Module is invisible to guest users so this should return null. $this->assertNull($event); } /** * Test that the event factory deals with invisible courses as an admin. * * @dataProvider get_event_factory_testcases * @param \stdClass $dbrow Row from the "database". */ public function test_event_factory_when_course_visibility_is_toggled_as_admin($dbrow): void { $legacyevent = $this->create_event($dbrow); $factory = \core_calendar\local\event\container::get_event_factory(); // Create a hidden course with an assignment. $course = $this->getDataGenerator()->create_course(['visible' => 0]); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $dbrow->id = $legacyevent->id; $dbrow->courseid = $course->id; $dbrow->instance = $moduleinstance->id; $dbrow->modulename = 'assign'; $event = $factory->create_instance($dbrow); // Module is still visible to admins even if the course is invisible. $this->assertInstanceOf(event_interface::class, $event); } /** * Test that the event factory deals with invisible courses as a student. * * @dataProvider get_event_factory_testcases * @param \stdClass $dbrow Row from the "database". */ public function test_event_factory_when_course_visibility_is_toggled_as_student($dbrow): void { $legacyevent = $this->create_event($dbrow); $factory = \core_calendar\local\event\container::get_event_factory(); // Create a hidden course with an assignment. $course = $this->getDataGenerator()->create_course(['visible' => 0]); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); // Enrol a student into this course. $student = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($student->id, $course->id); // Set the user to the student. $this->setUser($student); $dbrow->id = $legacyevent->id; $dbrow->courseid = $course->id; $dbrow->instance = $moduleinstance->id; $dbrow->modulename = 'assign'; $event = $factory->create_instance($dbrow); // Module is invisible to students if the course is invisible. $this->assertNull($event); } /** * Test that the event factory deals with invisible categorys as an admin. */ public function test_event_factory_when_category_visibility_is_toggled_as_admin(): void { // Create a hidden category. $category = $this->getDataGenerator()->create_category(['visible' => 0]); $eventdata = [ 'categoryid' => $category->id, 'eventtype' => 'category', ]; $legacyevent = $this->create_event($eventdata); $dbrow = $this->get_dbrow_from_skeleton((object) $eventdata); $dbrow->id = $legacyevent->id; $factory = \core_calendar\local\event\container::get_event_factory(); $event = $factory->create_instance($dbrow); // Module is still visible to admins even if the category is invisible. $this->assertInstanceOf(event_interface::class, $event); } /** * Test that the event factory deals with invisible categorys as an user. */ public function test_event_factory_when_category_visibility_is_toggled_as_user(): void { // Create a hidden category. $category = $this->getDataGenerator()->create_category(['visible' => 0]); $eventdata = [ 'categoryid' => $category->id, 'eventtype' => 'category', ]; $legacyevent = $this->create_event($eventdata); $dbrow = $this->get_dbrow_from_skeleton((object) $eventdata); $dbrow->id = $legacyevent->id; // Use a standard user. $user = $this->getDataGenerator()->create_user(); // Set the user to the student. $this->setUser($user); $factory = \core_calendar\local\event\container::get_event_factory(); $event = $factory->create_instance($dbrow); // Module is invisible to non-privileged users. $this->assertNull($event); } /** * Test that the event factory deals with invisible categorys as an guest. */ public function test_event_factory_when_category_visibility_is_toggled_as_guest(): void { // Create a hidden category. $category = $this->getDataGenerator()->create_category(['visible' => 0]); $eventdata = [ 'categoryid' => $category->id, 'eventtype' => 'category', ]; $legacyevent = $this->create_event($eventdata); $dbrow = $this->get_dbrow_from_skeleton((object) $eventdata); $dbrow->id = $legacyevent->id; // Set the user to the student. $this->setGuestUser(); $factory = \core_calendar\local\event\container::get_event_factory(); $event = $factory->create_instance($dbrow); // Module is invisible to guests. $this->assertNull($event); } /** * Test that the event factory deals with completion related events properly. */ public function test_event_factory_with_completion_related_event(): void { global $CFG; $CFG->enablecompletion = true; // Create the course we will be using. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Add the assignment. $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $assign = $generator->create_instance(array('course' => $course->id), array('completion' => 1)); // Create a completion event. $event = new \stdClass(); $event->name = 'An event'; $event->description = 'Event description'; $event->location = 'Event location'; $event->format = FORMAT_HTML; $event->eventtype = \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED; $event->userid = 1; $event->modulename = 'assign'; $event->instance = $assign->id; $event->categoryid = 0; $event->courseid = $course->id; $event->groupid = 0; $event->timestart = time(); $event->timesort = time(); $event->timemodified = time(); $event->timeduration = 0; $event->subscriptionid = null; $event->repeatid = 0; $legacyevent = $this->create_event($event); // Update the id of the event that was created. $event->id = $legacyevent->id; // Create the factory we are going to be testing the behaviour of. $factory = \core_calendar\local\event\container::get_event_factory(); // Check that we get the correct instance. $this->assertInstanceOf(event_interface::class, $factory->create_instance($event)); // Now, disable completion. $CFG->enablecompletion = false; // The result should now be null since we have disabled completion. $this->assertNull($factory->create_instance($event)); } /** * Checks that completed activities events do not show. */ public function test_event_factory_with_completed_module_related_event(): void { global $CFG, $DB; $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); $user = $this->getDataGenerator()->create_and_enrol($course); // Create an assign activity with a time set. $time = time(); $assign = $this->getDataGenerator()->create_module( 'assign', ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); // Create the event but set it to tomorrow. $CFG->enablecompletion = true; api::update_completion_date_event($assign->cmid, 'assign', $assign, $time + DAYSECS); $this->setUser($user); // Check that we get should be completed event. $this->assertCount(1, \core_calendar\local\event\container::get_event_vault()->get_events()); // Then Complete the activity. $completion = new \completion_info($course); $cmassign = get_coursemodule_from_id('assign', $assign->cmid); // This should trigger another call to the update_completion_date_event. $completion->update_state($cmassign, COMPLETION_COMPLETE, $user->id); // Check that we do not see the event anymore. $this->assertCount(0, \core_calendar\local\event\container::get_event_vault()->get_events()); } /** * Test that the event factory only returns an event if the logged in user * is enrolled in the course. */ public function test_event_factory_unenrolled_user(): void { $user = $this->getDataGenerator()->create_user(); // Create the course we will be using. $course = $this->getDataGenerator()->create_course(); // Add the assignment. $generator = $this->getDataGenerator()->get_plugin_generator('mod_lesson'); $lesson = $generator->create_instance(array('course' => $course->id)); // Create a user override event for the lesson. $event = new \stdClass(); $event->name = 'An event'; $event->description = 'Event description'; $event->location = 'Event location'; $event->format = FORMAT_HTML; $event->eventtype = 'close'; $event->userid = $user->id; $event->modulename = 'lesson'; $event->instance = $lesson->id; $event->categoryid = 0; $event->courseid = $course->id; $event->groupid = 0; $event->timestart = time(); $event->timesort = time(); $event->timemodified = time(); $event->timeduration = 0; $event->subscriptionid = null; $event->repeatid = 0; $legacyevent = $this->create_event($event); // Update the id of the event that was created. $event->id = $legacyevent->id; // Set the logged in user to the one we created. $this->setUser($user); // Create the factory we are going to be testing the behaviour of. $factory = \core_calendar\local\event\container::get_event_factory(); // The result should be null since the user is not enrolled in the // course the event is for. $this->assertNull($factory->create_instance($event)); // Now enrol the user in the course. $this->getDataGenerator()->enrol_user($user->id, $course->id); // Check that we get the correct instance. $this->assertInstanceOf(event_interface::class, $factory->create_instance($event)); } /** * Test that when course module is deleted all events are also deleted. */ public function test_delete_module_delete_events(): void { global $DB; $user = $this->getDataGenerator()->create_user(); // Create the course we will be using. $course = $this->getDataGenerator()->create_course(); $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]); foreach (\core_component::get_plugin_list('mod') as $modname => $unused) { try { $generator = $this->getDataGenerator()->get_plugin_generator('mod_'.$modname); } catch (\coding_exception $e) { // Module generator is not implemented. continue; } $module = $generator->create_instance(['course' => $course->id]); // Create bunch of events of different type (user override, group override, module event). $this->create_event(['userid' => $user->id, 'modulename' => $modname, 'instance' => $module->id]); $this->create_event(['groupid' => $group->id, 'modulename' => $modname, 'instance' => $module->id]); $this->create_event(['modulename' => $modname, 'instance' => $module->id]); $this->create_event(['modulename' => $modname, 'instance' => $module->id, 'courseid' => $course->id]); // Delete module and make sure all events are deleted. course_delete_module($module->cmid); $this->assertEmpty($DB->get_record('event', ['modulename' => $modname, 'instance' => $module->id])); } } /** * Test getting the event mapper. */ public function test_get_event_mapper(): void { $mapper = \core_calendar\local\event\container::get_event_mapper(); $this->assertInstanceOf(event_mapper_interface::class, $mapper); $this->assertInstanceOf(event_mapper::class, $mapper); $mapper2 = \core_calendar\local\event\container::get_event_mapper(); $this->assertTrue($mapper === $mapper2); } /** * Test cases for the get event factory test. */ public static function get_event_factory_testcases(): array { return [ 'Data set 1' => [ 'dbrow' => (object)[ 'name' => 'Test event', 'description' => 'Hello', 'format' => 1, 'categoryid' => 0, 'courseid' => 1, 'groupid' => 0, 'userid' => 1, 'repeatid' => 0, 'modulename' => 'assign', 'instance' => 2, 'eventtype' => 'due', 'timestart' => 1486396800, 'timeduration' => 0, 'timesort' => 1486396800, 'visible' => 1, 'timemodified' => 1485793098, 'subscriptionid' => null, 'location' => 'Test location', ] ], 'Data set 2' => [ 'dbrow' => (object)[ 'name' => 'Test event', 'description' => 'Hello', 'format' => 1, 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, 'repeatid' => 0, 'modulename' => 'assign', 'instance' => 2, 'eventtype' => 'due', 'timestart' => 1486396800, 'timeduration' => 0, 'timesort' => 1486396800, 'visible' => 1, 'timemodified' => 1485793098, 'subscriptionid' => null, 'location' => 'Test location', ] ] ]; } /** * Helper function to create calendar events using the old code. * * @param array $properties A list of calendar event properties to set * @return calendar_event|bool */ protected function create_event($properties = []) { $record = new \stdClass(); $record->name = 'event name'; $record->eventtype = 'site'; $record->timestart = time(); $record->timeduration = 0; $record->timesort = 0; $record->type = 1; $record->courseid = 0; $record->categoryid = 0; foreach ($properties as $name => $value) { $record->$name = $value; } $event = new \calendar_event($record); return $event->create($record, false); } /** * Pad out a basic DB row with basic information. * * @param \stdClass $skeleton the current skeleton * @return \stdClass */ protected function get_dbrow_from_skeleton($skeleton) { $dbrow = (object) [ 'name' => 'Name', 'description' => 'Description', 'format' => 1, 'categoryid' => 0, 'courseid' => 0, 'groupid' => 0, 'userid' => 0, 'repeatid' => 0, 'modulename' => '', 'instance' => 0, 'eventtype' => 'user', 'timestart' => 1486396800, 'timeduration' => 0, 'timesort' => 1486396800, 'visible' => 1, 'timemodified' => 1485793098, 'subscriptionid' => null, 'location' => 'Test location', ]; foreach ((array) $skeleton as $key => $value) { $dbrow->$key = $value; } return $dbrow; } } PK*\GT`J J tests/renderer_test.phpnu[. namespace core_calendar; /** * Renderer testcase. * * @covers \core_calendar_renderer * @package core_calendar * @copyright 2025 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class renderer_test extends \advanced_testcase { /** * Tests {@see \core_calendar_renderer::course_filter_selector()} shows course names correctly * depending on admin settings. */ public function test_course_filter_selector_names(): void { global $PAGE, $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); // Create 2 courses. $generator = self::getDataGenerator(); $course1 = $generator->create_course(['shortname' => 'C1', 'fullname' => 'Course 1']); $course2 = $generator->create_course(['shortname' => 'C2', 'fullname' => 'Course 2']); // User is enrolled in both courses. $user = $generator->create_user(); $generator->enrol_user($user->id, $course1->id, 'teacher'); $generator->enrol_user($user->id, $course2->id, 'teacher'); // Get course selector for user. $this->setUser($user); $renderer = $PAGE->get_renderer('core_calendar'); $html = $renderer->course_filter_selector(new \moodle_url('/')); // It should contain courses by fullname. $this->assertStringContainsString( '', $html, ); $this->assertStringContainsString( '', $html, ); // Turn on the option to show shortnames as well. set_config('courselistshortnames', true); $html = $renderer->course_filter_selector(new \moodle_url('/')); // It should contain courses by fullname and shortname. $this->assertStringContainsString( '', $html, ); $this->assertStringContainsString( '', $html, ); } } PK*\n]tests/lib_test.phpnu[. namespace core_calendar; /** * Class contaning unit tests for the calendar lib. * * @package core_calendar * @copyright 2017 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class lib_test extends \advanced_testcase { /** * Load required test libraries */ public static function setUpBeforeClass(): void { global $CFG; require_once("{$CFG->dirroot}/calendar/tests/helpers.php"); parent::setUpBeforeClass(); } /** * Tests set up */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test that the get_events() function only returns activity events that are enabled. */ public function test_get_events_with_disabled_module(): void { global $DB; $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $assigninstance = $assigngenerator->create_instance(['course' => $course->id]); $lessongenerator = $this->getDataGenerator()->get_plugin_generator('mod_lesson'); $lessoninstance = $lessongenerator->create_instance(['course' => $course->id]); $student = $generator->create_user(); $generator->enrol_user($student->id, $course->id, 'student'); $this->setUser($student); $events = [ [ 'name' => 'Start of assignment', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'assign', 'instance' => $assigninstance->id, 'eventtype' => 'due', 'timestart' => time(), 'timeduration' => 86400, 'visible' => 1 ], [ 'name' => 'Start of lesson', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'lesson', 'instance' => $lessoninstance->id, 'eventtype' => 'end', 'timestart' => time(), 'timeduration' => 86400, 'visible' => 1 ] ]; foreach ($events as $event) { \calendar_event::create($event, false); } $timestart = time() - 60; $timeend = time() + 60; // Get all events. $events = calendar_get_events($timestart, $timeend, true, 0, true); $this->assertCount(2, $events); // Disable the lesson module. $modulerecord = $DB->get_record('modules', ['name' => 'lesson']); $modulerecord->visible = 0; $DB->update_record('modules', $modulerecord); // Check that we only return the assign event. $events = calendar_get_events($timestart, $timeend, true, 0, true); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('assign', $event->modulename); } public function test_get_course_cached(): void { // Setup some test courses. $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); // Load courses into cache. $coursecache = null; calendar_get_course_cached($coursecache, $course1->id); calendar_get_course_cached($coursecache, $course2->id); calendar_get_course_cached($coursecache, $course3->id); // Verify the cache. $this->assertArrayHasKey($course1->id, $coursecache); $cachedcourse1 = $coursecache[$course1->id]; $this->assertEquals($course1->id, $cachedcourse1->id); $this->assertEquals($course1->shortname, $cachedcourse1->shortname); $this->assertEquals($course1->fullname, $cachedcourse1->fullname); $this->assertArrayHasKey($course2->id, $coursecache); $cachedcourse2 = $coursecache[$course2->id]; $this->assertEquals($course2->id, $cachedcourse2->id); $this->assertEquals($course2->shortname, $cachedcourse2->shortname); $this->assertEquals($course2->fullname, $cachedcourse2->fullname); $this->assertArrayHasKey($course3->id, $coursecache); $cachedcourse3 = $coursecache[$course3->id]; $this->assertEquals($course3->id, $cachedcourse3->id); $this->assertEquals($course3->shortname, $cachedcourse3->shortname); $this->assertEquals($course3->fullname, $cachedcourse3->fullname); } /** * Test the update_subscription() function. */ public function test_update_subscription(): void { $this->resetAfterTest(true); $subscription = new \stdClass(); $subscription->eventtype = 'site'; $subscription->name = 'test'; $id = calendar_add_subscription($subscription); $subscription = calendar_get_subscription($id); $subscription->name = 'awesome'; calendar_update_subscription($subscription); $sub = calendar_get_subscription($id); $this->assertEquals($subscription->name, $sub->name); $subscription = calendar_get_subscription($id); $subscription->name = 'awesome2'; $subscription->pollinterval = 604800; calendar_update_subscription($subscription); $sub = calendar_get_subscription($id); $this->assertEquals($subscription->name, $sub->name); $this->assertEquals($subscription->pollinterval, $sub->pollinterval); $subscription = new \stdClass(); $subscription->name = 'awesome4'; $this->expectException('coding_exception'); calendar_update_subscription($subscription); } public function test_add_subscription(): void { global $DB, $CFG; require_once($CFG->dirroot . '/lib/bennu/bennu.inc.php'); $this->resetAfterTest(true); // Test for Microsoft Outlook 2010. $subscription = new \stdClass(); $subscription->name = 'Microsoft Outlook 2010'; $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE; $subscription->eventtype = 'site'; $id = calendar_add_subscription($subscription); $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/ms_outlook_2010.ics'); $ical = new \iCalendar(); $ical->unserialize($calendar); $this->assertEquals($ical->parser_errors, array()); $sub = calendar_get_subscription($id); calendar_import_events_from_ical($ical, $sub->id); $count = $DB->count_records('event', array('subscriptionid' => $sub->id)); $this->assertEquals($count, 1); // Test for OSX Yosemite. $subscription = new \stdClass(); $subscription->name = 'OSX Yosemite'; $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE; $subscription->eventtype = 'site'; $id = calendar_add_subscription($subscription); $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/osx_yosemite.ics'); $ical = new \iCalendar(); $ical->unserialize($calendar); $this->assertEquals($ical->parser_errors, array()); $sub = calendar_get_subscription($id); calendar_import_events_from_ical($ical, $sub->id); $count = $DB->count_records('event', array('subscriptionid' => $sub->id)); $this->assertEquals($count, 1); // Test for Google Gmail. $subscription = new \stdClass(); $subscription->name = 'Google Gmail'; $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE; $subscription->eventtype = 'site'; $id = calendar_add_subscription($subscription); $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/google_gmail.ics'); $ical = new \iCalendar(); $ical->unserialize($calendar); $this->assertEquals($ical->parser_errors, array()); $sub = calendar_get_subscription($id); calendar_import_events_from_ical($ical, $sub->id); $count = $DB->count_records('event', array('subscriptionid' => $sub->id)); $this->assertEquals($count, 1); // Test for ICS file with repeated events. $subscription = new \stdClass(); $subscription->name = 'Repeated events'; $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE; $subscription->eventtype = 'site'; $id = calendar_add_subscription($subscription); $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/repeated_events.ics'); $ical = new \iCalendar(); $ical->unserialize($calendar); $this->assertEquals($ical->parser_errors, []); $sub = calendar_get_subscription($id); $output = calendar_import_events_from_ical($ical, $sub->id); $this->assertArrayHasKey('eventsimported', $output); $this->assertArrayHasKey('eventsskipped', $output); $this->assertArrayHasKey('eventsupdated', $output); $this->assertArrayHasKey('eventsdeleted', $output); $this->assertEquals(1, $output['eventsimported']); $this->assertEquals(0, $output['eventsskipped']); $this->assertEquals(0, $output['eventsupdated']); $this->assertEquals(0, $output['eventsdeleted']); } /** * Test for calendar_get_legacy_events() when there are user and group overrides. */ public function test_get_legacy_events_with_overrides(): void { $generator = $this->getDataGenerator(); $course = $generator->create_course(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); if (!isset($params['course'])) { $params['course'] = $course->id; } $instance = $plugingenerator->create_instance($params); // Create users. $useroverridestudent = $generator->create_user(); $group1student = $generator->create_user(); $group2student = $generator->create_user(); $group12student = $generator->create_user(); $nogroupstudent = $generator->create_user(); // Enrol users. $generator->enrol_user($useroverridestudent->id, $course->id, 'student'); $generator->enrol_user($group1student->id, $course->id, 'student'); $generator->enrol_user($group2student->id, $course->id, 'student'); $generator->enrol_user($group12student->id, $course->id, 'student'); $generator->enrol_user($nogroupstudent->id, $course->id, 'student'); // Create groups. $group1 = $generator->create_group(['courseid' => $course->id]); $group2 = $generator->create_group(['courseid' => $course->id]); // Add members to groups. $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group1student->id]); $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group2student->id]); $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group12student->id]); $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group12student->id]); $now = time(); // Events with the same module name, instance and event type. $events = [ [ 'name' => 'Assignment 1 due date', 'description' => '', 'location' => 'Test', 'format' => 0, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'due', 'timestart' => $now, 'timeduration' => 0, 'visible' => 1 ], [ 'name' => 'Assignment 1 due date - User override', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => 0, 'groupid' => 0, 'userid' => $useroverridestudent->id, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'due', 'timestart' => $now + 86400, 'timeduration' => 0, 'visible' => 1, 'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY ], [ 'name' => 'Assignment 1 due date - Group A override', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => $course->id, 'groupid' => $group1->id, 'userid' => 2, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'due', 'timestart' => $now + (2 * 86400), 'timeduration' => 0, 'visible' => 1, 'priority' => 1, ], [ 'name' => 'Assignment 1 due date - Group B override', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => $course->id, 'groupid' => $group2->id, 'userid' => 2, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'due', 'timestart' => $now + (3 * 86400), 'timeduration' => 0, 'visible' => 1, 'priority' => 2, ], ]; foreach ($events as $event) { \calendar_event::create($event, false); } $timestart = $now - 100; $timeend = $now + (3 * 86400); $groups = [$group1->id, $group2->id]; // Get user override events. $this->setUser($useroverridestudent); $events = calendar_get_legacy_events($timestart, $timeend, $useroverridestudent->id, $groups, $course->id); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date - User override', $event->name); // Get event for user with override but with the timestart and timeend parameters only covering the original event. $events = calendar_get_legacy_events($timestart, $now, $useroverridestudent->id, $groups, $course->id); $this->assertCount(0, $events); // Get events for user that does not belong to any group and has no user override events. $this->setUser($nogroupstudent); $events = calendar_get_legacy_events($timestart, $timeend, $nogroupstudent->id, $groups, $course->id); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date', $event->name); // Get events for user that belongs to groups A and B and has no user override events. $this->setUser($group12student); $events = calendar_get_legacy_events($timestart, $timeend, $group12student->id, $groups, $course->id); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date - Group A override', $event->name); // Get events for user that belongs to group A and has no user override events. $this->setUser($group1student); $events = calendar_get_legacy_events($timestart, $timeend, $group1student->id, $groups, $course->id); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date - Group A override', $event->name); // Add repeating events. $repeatingevents = [ [ 'name' => 'Repeating site event', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => SITEID, 'groupid' => 0, 'userid' => 2, 'repeatid' => $event->id, 'modulename' => '0', 'instance' => 0, 'eventtype' => 'site', 'timestart' => $now + 86400, 'timeduration' => 0, 'visible' => 1, ], [ 'name' => 'Repeating site event', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => SITEID, 'groupid' => 0, 'userid' => 2, 'repeatid' => $event->id, 'modulename' => '0', 'instance' => 0, 'eventtype' => 'site', 'timestart' => $now + (2 * 86400), 'timeduration' => 0, 'visible' => 1, ], ]; foreach ($repeatingevents as $event) { \calendar_event::create($event, false); } // Make sure repeating events are not filtered out. $events = calendar_get_legacy_events($timestart, $timeend, true, true, true); $this->assertCount(3, $events); } public function test_calendar_get_default_courses(): void { global $USER, $CFG; $this->resetAfterTest(true); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course1 = $generator->create_course(); $course2 = $generator->create_course(); $course3 = $generator->create_course(); $context = \context_course::instance($course1->id); $this->setAdminUser(); $admin = clone $USER; $teacher = $generator->create_user(); $generator->enrol_user($teacher->id, $course1->id, 'teacher'); $generator->enrol_user($admin->id, $course1->id, 'teacher'); $CFG->calendar_adminseesall = false; $courses = calendar_get_default_courses(); // Only enrolled in one course. $this->assertCount(1, $courses); $courses = calendar_get_default_courses($course2->id); // Enrolled course + current course. $this->assertCount(2, $courses); $CFG->calendar_adminseesall = true; $courses = calendar_get_default_courses(); // All courses + SITE. $this->assertCount(4, $courses); $courses = calendar_get_default_courses($course2->id); // All courses + SITE. $this->assertCount(4, $courses); $this->setUser($teacher); $CFG->calendar_adminseesall = false; $courses = calendar_get_default_courses(); // Only enrolled in one course. $this->assertCount(1, $courses); $courses = calendar_get_default_courses($course2->id); // Enrolled course only (ignore current). $this->assertCount(1, $courses); // This setting should not affect teachers. $CFG->calendar_adminseesall = true; $courses = calendar_get_default_courses(); // Only enrolled in one course. $this->assertCount(1, $courses); $courses = calendar_get_default_courses($course2->id); // Enrolled course only (ignore current). $this->assertCount(1, $courses); // Now, log out and test again. $this->setUser(); $CFG->calendar_adminseesall = false; $courses = calendar_get_default_courses(null, '*', false, $teacher->id); // Only enrolled in one course. $this->assertCount(1, $courses); $courses = calendar_get_default_courses($course2->id, '*', false, $teacher->id); // Enrolled course only (ignore current). $this->assertCount(1, $courses); // This setting should not affect teachers. $CFG->calendar_adminseesall = true; $courses = calendar_get_default_courses(null, '*', false, $teacher->id); // Only enrolled in one course. $this->assertCount(1, $courses); $courses = calendar_get_default_courses($course2->id, '*', false, $teacher->id); // Enrolled course only (ignore current). $this->assertCount(1, $courses); } /** * Confirm that the skip events flag causes the calendar_get_view function * to avoid querying for the calendar events. */ public function test_calendar_get_view_skip_events(): void { $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $skipnavigation = true; $skipevents = true; $event = create_event([ 'eventtype' => 'user', 'userid' => $user->id ]); $this->setUser($user); $calendar = \calendar_information::create(time() - 10, SITEID, null); list($data, $template) = calendar_get_view($calendar, 'day', $skipnavigation, $skipevents); $this->assertEmpty($data->events); $skipevents = false; list($data, $template) = calendar_get_view($calendar, 'day', $skipnavigation, $skipevents); $this->assertEquals($event->id, $data->events[0]->id); } public function test_calendar_get_allowed_event_types_course(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course1 = $generator->create_course(); // Has capability. $course2 = $generator->create_course(); // Doesn't have capability. $course3 = $generator->create_course(); // Not enrolled. $context1 = \context_course::instance($course1->id); $context2 = \context_course::instance($course2->id); $context3 = \context_course::instance($course3->id); $roleid = $generator->create_role(); $contexts = [$context1, $context2, $context3]; $enrolledcourses = [$course1, $course2]; foreach ($enrolledcourses as $course) { $generator->enrol_user($user->id, $course->id, 'student'); } foreach ($contexts as $context) { $generator->role_assign($roleid, $user->id, $context->id); } $this->setUser($user); // In general for all courses, they don't have the ability to add course events yet. $types = calendar_get_allowed_event_types(); $this->assertFalse($types['course']); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context1, true); assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context2, true); // The user only has the correct capability in course 1 so that is the only // one that should be in the results. $types = calendar_get_allowed_event_types($course1->id); $this->assertTrue($types['course']); // If calling function without specified course, there is still a course where they have it. $types = calendar_get_allowed_event_types(); $this->assertTrue($types['course']); assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context1, true); // The user only now has the correct capability in both course 1 and 2 so we // expect both to be in the results. $types = calendar_get_allowed_event_types($course3->id); $this->assertFalse($types['course']); // They now do not have permission in any course. $types = calendar_get_allowed_event_types(); $this->assertFalse($types['course']); } public function test_calendar_get_allowed_event_types_group_no_acces_to_diff_groups(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); $this->setUser($user); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true); // The user has the correct capability in the course but they aren't a member // of any of the groups and don't have the accessallgroups capability. $types = calendar_get_allowed_event_types($course->id); $this->assertTrue($types['course']); $this->assertFalse($types['group']); // Same result applies when not providing a specific course as they are only on one course. $types = calendar_get_allowed_event_types(); $this->assertTrue($types['course']); $this->assertFalse($types['group']); } public function test_calendar_get_allowed_event_types_group_no_groups(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); $this->setUser($user); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); // The user has the correct capability in the course but there are // no groups so we shouldn't see a group type. $types = calendar_get_allowed_event_types($course->id); $this->assertTrue($types['course']); $this->assertFalse($types['group']); // Same result applies when not providing a specific course as they are only on one course. $types = calendar_get_allowed_event_types(); $this->assertTrue($types['course']); $this->assertFalse($types['group']); } public function test_calendar_get_allowed_event_types_group_access_all_groups(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course1 = $generator->create_course(); $course2 = $generator->create_course(); $generator->create_group(array('courseid' => $course1->id)); $generator->create_group(array('courseid' => $course2->id)); $context1 = \context_course::instance($course1->id); $context2 = \context_course::instance($course2->id); $roleid = $generator->create_role(); $generator->enrol_user($user->id, $course1->id, 'student'); $generator->enrol_user($user->id, $course2->id, 'student'); $generator->role_assign($roleid, $user->id, $context1->id); $generator->role_assign($roleid, $user->id, $context2->id); $this->setUser($user); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context1, true); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context2, true); assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context1, true); assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context2, true); // The user has the correct capability in the course and has // the accessallgroups capability. $types = calendar_get_allowed_event_types($course1->id); $this->assertTrue($types['group']); // Same result applies when not providing a specific course as they are only on one course. $types = calendar_get_allowed_event_types(); $this->assertTrue($types['group']); } public function test_calendar_get_allowed_event_types_group_no_access_all_groups(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = \context_course::instance($course->id); $group1 = $generator->create_group(array('courseid' => $course->id)); $group2 = $generator->create_group(array('courseid' => $course->id)); $roleid = $generator->create_role(); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $user->id)); $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $user->id)); $this->setUser($user); assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true); // The user has the correct capability in the course but can't access // groups that they are not a member of. $types = calendar_get_allowed_event_types($course->id); $this->assertFalse($types['group']); // Same result applies when not providing a specific course as they are only on one course. $types = calendar_get_allowed_event_types(); $this->assertFalse($types['group']); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context, true); $types = calendar_get_allowed_event_types($course->id); $this->assertTrue($types['group']); // Same result applies when not providing a specific course as they are only on one course. $types = calendar_get_allowed_event_types(); $this->assertTrue($types['group']); } public function test_calendar_get_allowed_event_types_group_cap_no_groups(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $group = $generator->create_group(['courseid' => $course->id]); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:managegroupentries', CAP_ALLOW, $roleid, $context, true); $this->setUser($user); $types = calendar_get_allowed_event_types($course->id); $this->assertFalse($types['course']); $this->assertFalse($types['group']); // Check without specifying a course (same result as user only has one course). $types = calendar_get_allowed_event_types(); $this->assertFalse($types['course']); $this->assertFalse($types['group']); } public function test_calendar_get_allowed_event_types_group_cap_has_group(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $group = $generator->create_group(['courseid' => $course->id]); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); groups_add_member($group, $user); assign_capability('moodle/calendar:managegroupentries', CAP_ALLOW, $roleid, $context, true); $this->setUser($user); $types = calendar_get_allowed_event_types($course->id); $this->assertFalse($types['course']); $this->assertTrue($types['group']); // Check without specifying a course (same result as user only has one course). $types = calendar_get_allowed_event_types(); $this->assertFalse($types['course']); $this->assertTrue($types['group']); } public function test_calendar_get_allowed_event_types_group_cap_access_all_groups(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $group = $generator->create_group(['courseid' => $course->id]); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:managegroupentries', CAP_ALLOW, $roleid, $context, true); assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context, true); $this->setUser($user); $types = calendar_get_allowed_event_types($course->id); $this->assertFalse($types['course']); $this->assertTrue($types['group']); // Check without specifying a course (same result as user only has one course). $types = calendar_get_allowed_event_types(); $this->assertFalse($types['course']); $this->assertTrue($types['group']); } /** * This is a setup helper function that create some users, courses, groups and group memberships. * This is useful to prepare the environment for testing the calendar_set_filters function. * * @return array An array of ($users, $courses, $coursegroups) */ protected function setup_test_calendar_set_filters() { $generator = $this->getDataGenerator(); // Create some users. $users = []; $users[] = $generator->create_user(); $users[] = $generator->create_user(); $users[] = $generator->create_user(); // Create some courses. $courses = []; $courses[] = $generator->create_course(); $courses[] = $generator->create_course(); $courses[] = $generator->create_course(); $courses[] = $generator->create_course(); // Create some groups. $coursegroups = []; $coursegroups[$courses[0]->id] = []; $coursegroups[$courses[0]->id][] = $generator->create_group(['courseid' => $courses[0]->id]); $coursegroups[$courses[0]->id][] = $generator->create_group(['courseid' => $courses[0]->id]); $coursegroups[$courses[2]->id] = []; $coursegroups[$courses[2]->id][] = $generator->create_group(['courseid' => $courses[2]->id]); $coursegroups[$courses[2]->id][] = $generator->create_group(['courseid' => $courses[2]->id]); $coursegroups[$courses[3]->id] = []; $coursegroups[$courses[3]->id][] = $generator->create_group(['courseid' => $courses[3]->id]); $coursegroups[$courses[3]->id][] = $generator->create_group(['courseid' => $courses[3]->id]); // Create some enrolments and group memberships. $generator->enrol_user($users[0]->id, $courses[0]->id, 'student'); $generator->create_group_member(['groupid' => $coursegroups[$courses[0]->id][0]->id, 'userid' => $users[0]->id]); $generator->enrol_user($users[1]->id, $courses[0]->id, 'student'); $generator->create_group_member(['groupid' => $coursegroups[$courses[0]->id][1]->id, 'userid' => $users[1]->id]); $generator->enrol_user($users[0]->id, $courses[1]->id, 'student'); $generator->enrol_user($users[0]->id, $courses[2]->id, 'student'); return array($users, $courses, $coursegroups); } /** * This function tests calendar_set_filters for the case when user is not logged in. */ public function test_calendar_set_filters_not_logged_in(): void { $this->resetAfterTest(); list($users, $courses, $coursegroups) = $this->setup_test_calendar_set_filters(); $defaultcourses = calendar_get_default_courses(null, '*', false, $users[0]->id); list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses); $this->assertEqualsCanonicalizing( [$courses[0]->id, $courses[1]->id, $courses[2]->id, SITEID], array_values($courseids)); $this->assertFalse($groupids); $this->assertFalse($userid); } /** * This function tests calendar_set_filters for the case when no one is logged in, but a user id is provided. */ public function test_calendar_set_filters_not_logged_in_with_user(): void { $this->resetAfterTest(); list($users, $courses, $coursegroups) = $this->setup_test_calendar_set_filters(); $defaultcourses = calendar_get_default_courses(null, '*', false, $users[1]->id); list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses, false, $users[1]); $this->assertEquals(array($courses[0]->id, SITEID), array_values($courseids)); $this->assertEquals(array($coursegroups[$courses[0]->id][1]->id), $groupids); $this->assertEquals($users[1]->id, $userid); $defaultcourses = calendar_get_default_courses(null, '*', false, $users[0]->id); list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses, false, $users[0]); $this->assertEqualsCanonicalizing( [$courses[0]->id, $courses[1]->id, $courses[2]->id, SITEID], array_values($courseids)); $this->assertEquals(array($coursegroups[$courses[0]->id][0]->id), $groupids); $this->assertEquals($users[0]->id, $userid); } /** * This function tests calendar_set_filters for the case when user is logged in, but no user id is provided. */ public function test_calendar_set_filters_logged_in_no_user(): void { $this->resetAfterTest(); list($users, $courses, $coursegroups) = $this->setup_test_calendar_set_filters(); $this->setUser($users[0]); $defaultcourses = calendar_get_default_courses(null, '*', false, $users[0]->id); list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses, false); $this->assertEqualsCanonicalizing([$courses[0]->id, $courses[1]->id, $courses[2]->id, SITEID], array_values($courseids)); $this->assertEquals(array($coursegroups[$courses[0]->id][0]->id), $groupids); $this->assertEquals($users[0]->id, $userid); } /** * This function tests calendar_set_filters for the case when a user is logged in, but another user id is provided. */ public function test_calendar_set_filters_logged_in_another_user(): void { $this->resetAfterTest(); list($users, $courses, $coursegroups) = $this->setup_test_calendar_set_filters(); $this->setUser($users[0]); $defaultcourses = calendar_get_default_courses(null, '*', false, $users[1]->id); list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses, false, $users[1]); $this->assertEquals(array($courses[0]->id, SITEID), array_values($courseids)); $this->assertEquals(array($coursegroups[$courses[0]->id][1]->id), $groupids); $this->assertEquals($users[1]->id, $userid); } /** * This function tests calendar_set_filters for courses with separate group mode. */ public function test_calendar_set_filters_with_separate_group_mode(): void { global $DB; $this->resetAfterTest(); $generator = $this->getDataGenerator(); // Create users. $student1 = $generator->create_user(); $student2 = $generator->create_user(); $teacher1 = $generator->create_user(); $teacher2 = $generator->create_user(); // Create courses. $course1 = $generator->create_course([ 'shortname' => 'C1', 'groupmode' => 1, 'groupmodeforce' => 1, ]); $course2 = $generator->create_course([ 'shortname' => 'C2', 'groupmode' => 1, 'groupmodeforce' => 1, ]); $course1context = \context_course::instance($course1->id); $course2context = \context_course::instance($course2->id); // Create groups. $group1 = $generator->create_group([ 'name' => 'G1-C1', 'courseid' => $course1->id, ]); $group2 = $generator->create_group([ 'name' => 'G1-C2', 'courseid' => $course2->id, ]); $group3 = $generator->create_group([ 'name' => 'G2-C2', 'courseid' => $course2->id, ]); // Modify the capabilities. $editingteacherroleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); assign_capability( 'moodle/site:accessallgroups', CAP_PREVENT, $editingteacherroleid, $course1context->id, true ); assign_capability( 'moodle/site:accessallgroups', CAP_PREVENT, $editingteacherroleid, $course2context->id, true ); // Enrol users. $generator->enrol_user($student1->id, $course1->id, 'student'); $generator->enrol_user($teacher1->id, $course1->id, 'editingteacher'); $generator->enrol_user($student1->id, $course2->id, 'student'); $generator->enrol_user($student2->id, $course2->id, 'student'); $generator->enrol_user($teacher1->id, $course2->id, 'editingteacher'); $generator->enrol_user($teacher2->id, $course2->id, 'editingteacher'); // Group memberships. $generator->create_group_member([ 'groupid' => $group1->id, 'userid' => $student1->id, ]); $generator->create_group_member([ 'groupid' => $group1->id, 'userid' => $teacher1->id, ]); $generator->create_group_member([ 'groupid' => $group2->id, 'userid' => $student1->id, ]); $generator->create_group_member([ 'groupid' => $group2->id, 'userid' => $teacher1->id, ]); $generator->create_group_member([ 'groupid' => $group3->id, 'userid' => $student2->id, ]); $generator->create_group_member([ 'groupid' => $group3->id, 'userid' => $teacher2->id, ]); // Test teacher1. $this->setUser($teacher1); $defaultcourses = calendar_get_default_courses( null, '*', false, $teacher1->id ); [$courseids, $groupids] = calendar_set_filters( $defaultcourses, false, $teacher1 ); // Teacher1 can see SITE, C1, G1-C1, C2, G1-C2. $this->assertCount(3, $courseids); // SITE, C1, C2. $this->assertCount(2, $groupids); // G1-C1, G1-C2. $courseidskey = array_fill_keys($courseids, null); $this->assertArrayHasKey(SITEID, $courseidskey); $this->assertArrayHasKey($course1->id, $courseidskey); $this->assertArrayHasKey($course2->id, $courseidskey); $groupidskey = array_fill_keys($groupids, null); $this->assertArrayHasKey($group1->id, $groupidskey); $this->assertArrayHasKey($group2->id, $groupidskey); $this->assertArrayNotHasKey($group3->id, $groupidskey); // Test teacher2. $this->setUser($teacher2); $defaultcourses = calendar_get_default_courses( null, '*', false, $teacher2->id ); [$courseids, $groupids] = calendar_set_filters( $defaultcourses, false, $teacher2 ); // Teacher2 can see SITE, C2, G2-C2. $this->assertCount(2, $courseids); // SITE, C2. $this->assertCount(1, $groupids); // G2-C2. $courseidskey = array_fill_keys($courseids, null); $this->assertArrayHasKey(SITEID, $courseidskey); $this->assertArrayHasKey($course2->id, $courseidskey); $groupidskey = array_fill_keys($groupids, null); $this->assertArrayHasKey($group3->id, $groupidskey); $this->assertArrayNotHasKey($group1->id, $groupidskey); $this->assertArrayNotHasKey($group2->id, $groupidskey); // Modify the capabilities. assign_capability( 'moodle/site:accessallgroups', CAP_ALLOW, $editingteacherroleid, $course2context->id, true ); $defaultcourses = calendar_get_default_courses( null, '*', false, $teacher2->id ); [$courseids, $groupids] = calendar_set_filters( $defaultcourses, false, $teacher2 ); // Teacher2 can see SITE, C2, G1-C2, G2-C2. $this->assertCount(2, $courseids); // SITE, C2. $this->assertCount(2, $groupids); // G1-C2, G2-C2. $groupidskey = array_fill_keys($groupids, null); $this->assertArrayHasKey($group2->id, $groupidskey); $this->assertArrayHasKey($group3->id, $groupidskey); $this->assertArrayNotHasKey($group1->id, $groupidskey); } /** * Test for calendar_view_event_allowed for course event types. */ public function test_calendar_view_event_allowed_course_event(): void { global $USER; $this->setAdminUser(); $generator = $this->getDataGenerator(); // A student in a course. $student = $generator->create_user(); // Some user not enrolled in any course. $someuser = $generator->create_user(); // A course with manual enrolments. $manualcourse = $generator->create_course(); // Enrol the student to the manual enrolment course. $generator->enrol_user($student->id, $manualcourse->id); // A course that allows guest access. $guestcourse = $generator->create_course( (object)[ 'shortname' => 'guestcourse', 'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED, 'enrol_guest_password_0' => '' ]); $manualevent = (object)[ 'name' => 'Manual course event', 'description' => '', 'format' => 1, 'categoryid' => 0, 'courseid' => $manualcourse->id, 'groupid' => 0, 'userid' => $USER->id, 'modulename' => 0, 'instance' => 0, 'eventtype' => 'course', 'timestart' => time(), 'timeduration' => 86400, 'visible' => 1 ]; $caleventmanual = \calendar_event::create($manualevent, false); // Create a course event for the course with guest access. $guestevent = clone $manualevent; $guestevent->name = 'Guest course event'; $guestevent->courseid = $guestcourse->id; $caleventguest = \calendar_event::create($guestevent, false); // Viewing as admin. $this->assertTrue(calendar_view_event_allowed($caleventmanual)); $this->assertTrue(calendar_view_event_allowed($caleventguest)); // Viewing as someone enrolled in a course. $this->setUser($student); $this->assertTrue(calendar_view_event_allowed($caleventmanual)); // Viewing as someone not enrolled in any course. $this->setUser($someuser); // Viewing as someone not enrolled in a course without guest access on. $this->assertFalse(calendar_view_event_allowed($caleventmanual)); // Viewing as someone not enrolled in a course with guest access on. $this->assertTrue(calendar_view_event_allowed($caleventguest)); } /** * Test for calendar_get_export_token for current user. */ public function test_calendar_get_export_token_for_current_user(): void { global $USER, $DB, $CFG; $this->setAdminUser(); // Get my token. $authtoken = calendar_get_export_token($USER); $expected = sha1($USER->id . $DB->get_field('user', 'password', ['id' => $USER->id]) . $CFG->calendar_exportsalt); $this->assertEquals($expected, $authtoken); } /** * Test for calendar_get_export_token for another user. */ public function test_calendar_get_export_token_for_another_user(): void { global $CFG; // Get any user token. $generator = $this->getDataGenerator(); $user = $generator->create_user(); // Get other user token. $authtoken = calendar_get_export_token($user); $expected = sha1($user->id . $user->password . $CFG->calendar_exportsalt); $this->assertEquals($expected, $authtoken); } /** * Test calendar_can_manage_user_event for different users. * * @covers ::calendar_can_manage_user_event */ public function test_calendar_can_manage_user_event(): void { global $DB, $USER; $generator = $this->getDataGenerator(); $sitecontext = \context_system::instance(); $this->resetAfterTest(); $this->setAdminUser(); $user1 = $generator->create_user(); $user2 = $generator->create_user(); $adminevent = create_event([ 'eventtype' => 'user', 'userid' => $USER->id, ]); $this->setUser($user1); $user1event = create_event([ 'name' => 'user1 event', 'eventtype' => 'user', 'userid' => $user1->id, ]); $this->setUser($user2); $user2event = create_event([ 'name' => 'user2 event', 'eventtype' => 'user', 'userid' => $user2->id, ]); $this->setUser($user1); $result = calendar_can_manage_user_event($user1event); $this->assertEquals(true, $result); $result = calendar_can_manage_user_event($user2event); $this->assertEquals(false, $result); $sitemanager = $generator->create_user(); $managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']); role_assign($managerroleid, $sitemanager->id, $sitecontext->id); $this->setUser($sitemanager); $result = calendar_can_manage_user_event($user1event); $this->assertEquals(true, $result); $result = calendar_can_manage_user_event($adminevent); $this->assertEquals(false, $result); } /** * Data provider for {@see test_calendar_format_event_location} * * @return array[] */ public static function calendar_format_event_location_provider(): array { return [ 'Empty' => ['', ''], 'Text' => ['Barcelona', 'Barcelona'], 'Link (http)' => ['http://example.com', 'http://example.com'], 'Link (https)' => ['https://example.com', 'https://example.com'], ]; } /** * Test formatting event location * * @param string $location * @param string $expectedpattern * * @covers ::calendar_format_event_location * @dataProvider calendar_format_event_location_provider */ public function test_calendar_format_event_location(string $location, string $expectedpattern): void { $this->resetAfterTest(); $this->setAdminUser(); $event = create_event(['location' => $location]); $this->assertMatchesRegularExpression("|^({$expectedpattern})$|", calendar_format_event_location($event)); } } PK*\-&tests/repeat_event_collection_test.phpnu[. /** * Repeat event collection tests. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar; use core_calendar\local\event\entities\event; use core_calendar\local\event\entities\repeat_event_collection; 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\factories\event_factory_interface; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); /** * Repeat event collection tests. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class repeat_event_collection_test extends \advanced_testcase { /** * Test that the collection id is set to the parent id if the repeat id * is falsey. */ public function test_parent_id_no_repeat_id(): void { $this->resetAfterTest(true); $dbrow = (object) [ 'id' => 123122131, 'repeatid' => null ]; $factory = new core_calendar_repeat_event_collection_event_test_factory(); $collection = new repeat_event_collection($dbrow, $factory); $this->assertEquals($dbrow->id, $collection->get_id()); } /** * Test that the repeat id is set to the parent id if the repeat id * is not falsey (even if the parent id is provided). */ public function test_parent_id_and_repeat_id(): void { $this->resetAfterTest(true); $dbrow = (object) [ 'id' => 123122131, 'repeatid' => 5647839 ]; $factory = new core_calendar_repeat_event_collection_event_test_factory(); $collection = new repeat_event_collection($dbrow, $factory); $this->assertEquals($dbrow->repeatid, $collection->get_id()); } /** * Test that an empty collection is valid. */ public function test_empty_collection(): void { $this->resetAfterTest(true); $this->setAdminUser(); $event = $this->create_event([ // This causes the code to set the repeat id on this record // but not create any repeat event records. 'repeat' => 1, 'repeats' => 0 ]); $dbrow = (object) [ 'id' => $event->id, 'repeatid' => null ]; $factory = new core_calendar_repeat_event_collection_event_test_factory(); // Event collection with no repeats. $collection = new repeat_event_collection($dbrow, $factory); $this->assertEquals($event->id, $collection->get_id()); $this->assertEquals(0, $collection->get_num()); $this->assertNull($collection->getIterator()->next()); } /** * Test that a collection with values behaves correctly. */ public function test_values_collection(): void { $this->resetAfterTest(true); $this->setAdminUser(); $factory = new core_calendar_repeat_event_collection_event_test_factory(); $event = $this->create_event([ // This causes the code to set the repeat id on this record // but not create any repeat event records. 'repeat' => 1, 'repeats' => 0 ]); $parentid = $event->id; $dbrow = (object) [ 'id' => $parentid, 'repeatid' => null ]; $repeats = []; for ($i = 1; $i < 4; $i++) { $record = $this->create_event([ 'name' => sprintf('repeat %d', $i), 'repeatid' => $parentid ]); // Index by name so that we don't have to rely on sorting // when doing the comparison later. $repeats[$record->name] = $record; } // Event collection with no repeats. $collection = new repeat_event_collection($dbrow, $factory); $this->assertEquals($parentid, $collection->get_id()); $this->assertEquals(count($repeats), $collection->get_num()); foreach ($collection as $index => $event) { $name = $event->get_name(); $this->assertEquals($repeats[$name]->name, $name); } } /** * Helper function to create calendar events using the old code. * * @param array $properties A list of calendar event properties to set * @return calendar_event */ protected function create_event($properties = []) { $record = new \stdClass(); $record->name = 'event name'; $record->eventtype = 'site'; $record->repeat = 0; $record->repeats = 0; $record->timestart = time(); $record->timeduration = 0; $record->timesort = 0; $record->type = 1; $record->courseid = 0; $record->categoryid = 0; foreach ($properties as $name => $value) { $record->$name = $value; } $event = new \calendar_event($record); return $event->create($record, false); } } /** * Test event factory. * * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_calendar_repeat_event_collection_event_test_factory implements event_factory_interface { public function create_instance(\stdClass $dbrow) { $identity = function($id) { return $id; }; return new event( $dbrow->id, $dbrow->name, new event_description($dbrow->description, $dbrow->format), new coursecat_proxy($dbrow->categoryid), new std_proxy($dbrow->courseid, $identity), new std_proxy($dbrow->groupid, $identity), new std_proxy($dbrow->userid, $identity), $dbrow->repeatid ? new repeat_event_collection($dbrow, $this) : null, new std_proxy($dbrow->instance, $identity), $dbrow->type, new event_times( (new \DateTimeImmutable())->setTimestamp($dbrow->timestart), (new \DateTimeImmutable())->setTimestamp($dbrow->timestart + $dbrow->timeduration), (new \DateTimeImmutable())->setTimestamp($dbrow->timesort ? $dbrow->timesort : $dbrow->timestart), (new \DateTimeImmutable())->setTimestamp($dbrow->timemodified), (new \DateTimeImmutable())->setTimestamp(usergetmidnight($dbrow->timesort)) ), !empty($dbrow->visible), new std_proxy($dbrow->subscriptionid, $identity), $dbrow->location, $dbrow->component ); } } PK*\6Y#tests/calendar_information_test.phpnu[. namespace core_calendar; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/helpers.php'); /** * Unit tests for calendar_information. * * @package core_calendar * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class calendar_information_test extends \advanced_testcase { /** * Helper to mock a course and category structure. * * @return array */ protected function mock_structure() { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $categories = []; $courses = []; $categories['A'] = $generator->create_category(['name' => 'A']); $courses['A.1'] = $generator->create_course(['category' => $categories['A']->id]); $courses['A.2'] = $generator->create_course(['category' => $categories['A']->id]); $categories['A1'] = $generator->create_category(['name' => 'A1', 'parent' => $categories['A']->id]); $courses['A1.1'] = $generator->create_course(['category' => $categories['A1']->id]); $courses['A1.2'] = $generator->create_course(['category' => $categories['A1']->id]); $categories['A1i'] = $generator->create_category(['name' => 'A1i', 'parent' => $categories['A1']->id]); $categories['A1ii'] = $generator->create_category(['name' => 'A1ii', 'parent' => $categories['A1']->id]); $categories['A2'] = $generator->create_category(['name' => 'A2', 'parent' => $categories['A']->id]); $courses['A2.1'] = $generator->create_course(['category' => $categories['A2']->id]); $courses['A2.2'] = $generator->create_course(['category' => $categories['A2']->id]); $categories['A2i'] = $generator->create_category(['name' => 'A2i', 'parent' => $categories['A2']->id]); $categories['A2ii'] = $generator->create_category(['name' => 'A2ii', 'parent' => $categories['A2']->id]); $categories['B'] = $generator->create_category(['name' => 'B']); $courses['B.1'] = $generator->create_course(['category' => $categories['B']->id]); $courses['B.2'] = $generator->create_course(['category' => $categories['B']->id]); $categories['B1'] = $generator->create_category(['name' => 'B1', 'parent' => $categories['B']->id]); $categories['B1i'] = $generator->create_category(['name' => 'B1i', 'parent' => $categories['B1']->id]); $categories['B1ii'] = $generator->create_category(['name' => 'B1ii', 'parent' => $categories['B1']->id]); $categories['B2'] = $generator->create_category(['name' => 'B2', 'parent' => $categories['B']->id]); $categories['B2i'] = $generator->create_category(['name' => 'B2i', 'parent' => $categories['B2']->id]); $categories['B2ii'] = $generator->create_category(['name' => 'B2ii', 'parent' => $categories['B2']->id]); $categories['C'] = $generator->create_category(['name' => 'C']); return [$courses, $categories]; } /** * Given a user has no enrolments. * And I ask for the site information. * Then I should see the site. * And I should see no other courses. * And I should see no categories. */ public function test_site_visibility_no_enrolment(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $this->setUser($user); $calendar = \calendar_information::create(time(), SITEID, null); $this->assertCount(1, $calendar->courses); $this->assertCount(0, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertEquals(SITEID, reset($calendar->courses)); $this->assertEquals(\context_system::instance(), $calendar->context); } /** * Given a user has no enrolments. * And I ask for a category. * Then I should see the category. * And I should see the category parents. * And I should see the category descendants. * And I should see the site course. * And I should see no other courses. */ public function test_site_visibility_no_enrolment_category(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $this->setUser($user); $category = $categories['A1']; $calendar = \calendar_information::create(time(), SITEID, $category->id); $this->assertCount(1, $calendar->courses); $this->assertCount(4, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertEquals(SITEID, reset($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1i']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1ii']->id, array_flip($calendar->categories)); $this->assertEquals(\context_coursecat::instance($category->id), $calendar->context); } /** * Given a user has a role assignment to manage a category. * And I ask for the site information. * Then I should see that category. * And I should see the category parents. * And I should see the category descendants. * And I should see the site course. * And I should see no other courses. */ public function test_site_visibility_category_manager_site(): void { global $DB; $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $category = $categories['A1']; $roles = $DB->get_records('role', [], '', 'shortname, id'); $generator->role_assign($roles['manager']->id, $user->id, \context_coursecat::instance($category->id)); $this->setUser($user); $calendar = \calendar_information::create(time(), SITEID, null); $this->assertCount(1, $calendar->courses); $this->assertCount(4, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertEquals(SITEID, reset($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1i']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1ii']->id, array_flip($calendar->categories)); $this->assertEquals(\context_system::instance(), $calendar->context); } /** * Given a user has a role assignment to manage a category. * And I ask for that category. * Then I should see that category. * And I should see the category parents. * And I should see the category descendants. * And I should see the site course. * And I should see no other courses. */ public function test_site_visibility_category_manager_own_category(): void { global $DB; $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $category = $categories['A1']; $roles = $DB->get_records('role', [], '', 'shortname, id'); $generator->role_assign($roles['manager']->id, $user->id, \context_coursecat::instance($category->id)); $this->setUser($user); $calendar = \calendar_information::create(time(), SITEID, $category->id); $this->assertCount(1, $calendar->courses); $this->assertCount(4, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertEquals(SITEID, reset($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1i']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1ii']->id, array_flip($calendar->categories)); $this->assertEquals(\context_coursecat::instance($category->id), $calendar->context); } /** * Given a user has a role assignment to manage a category. * And I ask for the parent of that category. * Then I should see that category. * And I should see the category parents. * And I should see the category descendants. * And I should see the site course. * And I should see no other courses. */ public function test_site_visibility_category_manager_parent_category(): void { global $DB; $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $category = $categories['A1']; $roles = $DB->get_records('role', [], '', 'shortname, id'); $generator->role_assign($roles['manager']->id, $user->id, \context_coursecat::instance($category->id)); $this->setUser($user); $calendar = \calendar_information::create(time(), SITEID, $category->parent); $this->assertCount(1, $calendar->courses); $this->assertCount(7, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertEquals(SITEID, reset($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1i']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1ii']->id, array_flip($calendar->categories)); $this->assertEquals(\context_coursecat::instance($category->parent), $calendar->context); } /** * Given a user has a role assignment to manage a category. * And I ask for a child of that category. * Then I should see that category. * And I should see the category parents. * And I should see the category descendants. * And I should see the site course. * And I should see no other courses. */ public function test_site_visibility_category_manager_child_category(): void { global $DB; $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $enrolledcategory = $categories['A1']; $category = $categories['A1i']; $roles = $DB->get_records('role', [], '', 'shortname, id'); $generator->role_assign($roles['manager']->id, $user->id, \context_coursecat::instance($enrolledcategory->id)); $this->setUser($user); $calendar = \calendar_information::create(time(), SITEID, $category->id); $this->assertCount(1, $calendar->courses); $this->assertCount(3, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertEquals(SITEID, reset($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1i']->id, array_flip($calendar->categories)); $this->assertEquals(\context_coursecat::instance($category->id), $calendar->context); } /** * Given a user has an enrolment in a single course. * And I ask for the site information. * Then I should see the site. * And I should see the course I am enrolled in. * And I should see the category that my enrolled course is in. * And I should see the parents of the category that my enrolled course is in. */ public function test_site_visibility_single_course_site(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $courses['A1.1']; $category = \core_course_category::get($course->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $course->id); $this->setUser($user); // Viewing the site as a whole. // Should see all courses that this user is enrolled in, and their // categories, and those categories parents. $calendar = \calendar_information::create(time(), SITEID, null); $this->assertCount(2, $calendar->courses); $this->assertCount(2, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertArrayHasKey($course->id, array_flip($calendar->courses)); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertEquals(\context_system::instance(), $calendar->context); } /** * Given a user has an enrolment in a single course. * And I ask for the course information. * Then I should see the site. * And I should see that course. * And I should see the category of that course. * And I should see the parents of that course category. */ public function test_site_visibility_single_course_course_course(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $courses['A1.1']; $category = \core_course_category::get($course->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $course->id); $this->setUser($user); $time = time(); // Viewing the course calendar. // Should see just this course, and all parent categories. $calendar = \calendar_information::create($time, $course->id, null); $this->assertCount(2, $calendar->courses); $this->assertCount(2, $calendar->categories); $this->assertEquals($course->id, $calendar->courseid); $this->assertArrayHasKey($course->id, array_flip($calendar->courses)); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertEquals(\context_course::instance($course->id), $calendar->context); // Viewing the course calendar while specifying the category too. // The category is essentially ignored. No change expected. $calendarwithcategory = \calendar_information::create($time, $course->id, $category->id); $this->assertEquals($calendar, $calendarwithcategory); // Viewing the course calendar while specifying the wrong category. // The category is essentially ignored. No change expected. $calendarwithwrongcategory = \calendar_information::create($time, $course->id, $wrongcategory->id); $this->assertEquals($calendar, $calendarwithwrongcategory); } /** * Given a user has an enrolment in a single course. * And I ask for the category information for the category my course is in. * Then I should see that category. * And I should see the category parents. * And I should see the category descendants. * And I should see the site. * And I should see my course. * And I should see no other courses. * And I should see no categories. */ public function test_site_visibility_single_course_category(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $courses['A1.1']; $category = \core_course_category::get($course->category); $generator->enrol_user($user->id, $course->id); $this->setUser($user); // Viewing the category calendar. // Should see all courses that this user is enrolled in within this // category, plus the site course, plus the category that course is // in and it's parents, and it's children. $calendar = \calendar_information::create(time(), SITEID, $category->id); $this->assertCount(2, $calendar->courses); $this->assertCount(4, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertArrayHasKey($course->id, array_flip($calendar->courses)); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1i']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1ii']->id, array_flip($calendar->categories)); $this->assertEquals(\context_coursecat::instance($category->id), $calendar->context); } /** * Given a user has an enrolment in a single course. * And I ask for the category information for the parent of the category my course is in. * Then I should see that category. * And I should see the category parents. * And I should see the category descendants. * And I should see the site. * And I should see my course. * And I should see no other courses. * And I should see no categories. */ public function test_site_visibility_single_course_parent_category(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $courses['A1.1']; $category = \core_course_category::get($course->category); $generator->enrol_user($user->id, $course->id); $this->setUser($user); // Viewing the category calendar. // Should see all courses that this user is enrolled in within this // category, plus the site course, plus the category that course is // in and it's parents, and it's children. $calendar = \calendar_information::create(time(), SITEID, $category->parent); $this->assertCount(2, $calendar->courses); $this->assertCount(7, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertArrayHasKey($course->id, array_flip($calendar->courses)); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1i']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1ii']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A2']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A2i']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A2ii']->id, array_flip($calendar->categories)); $this->assertEquals(\context_coursecat::instance($category->parent), $calendar->context); } /** * Given a user has an enrolment in a single course. * And I ask for the category information for the sibling of the category my course is in. * Then I should see that category. * And I should see the category parents. * And I should see the category descendants. * And I should see the site. * And I should see my course. * And I should see no other courses. * And I should see no categories. */ public function test_site_visibility_single_course_sibling_category(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $courses['A1.1']; $category = $categories['A2']; $generator->enrol_user($user->id, $course->id); $this->setUser($user); // Viewing the category calendar. // Should see all courses that this user is enrolled in within this // category, plus the site course, plus the category that course is // in and it's parents, and it's children. $calendar = \calendar_information::create(time(), SITEID, $category->id); $this->assertCount(1, $calendar->courses); $this->assertCount(4, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A2']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A2i']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A2ii']->id, array_flip($calendar->categories)); $this->assertEquals(\context_coursecat::instance($category->id), $calendar->context); } /** * Given a user has an enrolment in a single course. * And I ask for the category information for a different category to the one my course is in. * Then I should see that category. * And I should see the category parents. * And I should see the category descendants. * And I should see the site. * And I should see not see my course. * And I should see no other courses. * And I should see no categories. */ public function test_site_visibility_single_course_different_category(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $courses['A1.1']; $category = \core_course_category::get($course->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $course->id); $this->setUser($user); // Viewing the category calendar for a category the user doesn't have any enrolments in. // Should see that category, and all categories underneath it. $calendar = \calendar_information::create(time(), SITEID, $wrongcategory->id); $this->assertCount(1, $calendar->courses); $this->assertCount(4, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertArrayHasKey($categories['B']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['B1']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['B1i']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['B1ii']->id, array_flip($calendar->categories)); $this->assertEquals(\context_coursecat::instance($wrongcategory->id), $calendar->context); } /** * Given a user has an enrolment in two courses in the same category. * And I ask for the site information. * Then I should see the site. * And I should see the course I am enrolled in. * And I should see the category that my enrolled course is in. * And I should see the parents of the category that my enrolled course is in. */ public function test_site_visibility_two_courses_one_category_site(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['A.2']; $category = \core_course_category::get($coursea->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); $this->setUser($user); // Viewing the site azs a whole. // Should see all courses that this user is enrolled in. $calendar = \calendar_information::create(time(), SITEID, null); $this->assertCount(3, $calendar->courses); $this->assertCount(1, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertArrayHasKey($coursea->id, array_flip($calendar->courses)); $this->assertArrayHasKey($courseb->id, array_flip($calendar->courses)); $this->assertEquals(\context_system::instance(), $calendar->context); } /** * Given a user has an enrolment in two courses in the same category. * And I ask for the course information. * Then I should see the site. * And I should see that course. * And I should see the category that my enrolled courses are in. * And I should see the parents of the category that my enrolled course are in. */ public function test_site_visibility_two_courses_one_category_course(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['A.2']; $category = \core_course_category::get($coursea->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); $this->setUser($user); $time = time(); // Viewing the course calendar. // Should see all courses that this user is enrolled in. $calendar = \calendar_information::create($time, $coursea->id, null); $this->assertCount(2, $calendar->courses); $this->assertCount(1, $calendar->categories); $this->assertEquals($coursea->id, $calendar->courseid); $this->assertArrayHasKey($coursea->id, array_flip($calendar->courses)); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertEquals(\context_course::instance($coursea->id), $calendar->context); // Viewing the course calendar while specifying the category too. // The category is essentially ignored. No change expected. $calendarwithcategory = \calendar_information::create($time, $coursea->id, $category->id); $this->assertEquals($calendar, $calendarwithcategory); // Viewing the course calendar while specifying the wrong category. // The category is essentially ignored. No change expected. $calendarwithwrongcategory = \calendar_information::create($time, $coursea->id, $wrongcategory->id); $this->assertEquals($calendar, $calendarwithwrongcategory); } /** * Given a user has an enrolment in two courses in the same category. * And I ask for the course information of the second course. * Then I should see the site. * And I should see that course. * And I should see the category that my enrolled courses are in. * And I should see the parents of the category that my enrolled course are in. */ public function test_site_visibility_two_courses_one_category_courseb(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['A.2']; $category = \core_course_category::get($coursea->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); $this->setUser($user); $time = time(); // Viewing the other course calendar. // Should see all courses that this user is enrolled in. $calendar = \calendar_information::create($time, $courseb->id, null); $this->assertCount(2, $calendar->courses); $this->assertCount(1, $calendar->categories); $this->assertEquals($courseb->id, $calendar->courseid); $this->assertArrayHasKey($courseb->id, array_flip($calendar->courses)); $this->assertArrayNotHasKey($coursea->id, array_flip($calendar->courses)); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertEquals(\context_course::instance($courseb->id), $calendar->context); // Viewing the course calendar while specifying the category too. // The category is essentially ignored. No change expected. $calendarwithcategory = \calendar_information::create($time, $courseb->id, $category->id); $this->assertEquals($calendar, $calendarwithcategory); // Viewing the course calendar while specifying the wrong category. // The category is essentially ignored. No change expected. $calendarwithcategory = \calendar_information::create($time, $courseb->id, $wrongcategory->id); $this->assertEquals($calendar, $calendarwithcategory); } /** * Given a user has an enrolment in two courses in the same category. * And I ask for the category information. * Then I should see the site. * And I should see that course. * And I should see the category that my enrolled courses are in. * And I should see the parents of the category that my enrolled course are in. */ public function test_site_visibility_two_courses_one_category_category(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['A.2']; $category = \core_course_category::get($coursea->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); $this->setUser($user); // Viewing the category calendar. // Should see all courses that this user is enrolled in. $calendar = \calendar_information::create(time(), SITEID, $category->id); $this->assertCount(3, $calendar->courses); $this->assertCount(7, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertArrayHasKey($coursea->id, array_flip($calendar->courses)); $this->assertArrayHasKey($courseb->id, array_flip($calendar->courses)); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertEquals(\context_coursecat::instance($category->id), $calendar->context); } /** * Given a user has an enrolment in two courses in the same category. * And I ask for the categoy information of a different course. * Then I should see the site. * And I should see that course. * And I should see the category that my enrolled courses are in. * And I should see the parents of the category that my enrolled course are in. */ public function test_site_visibility_two_courses_one_category_othercategory(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['A.2']; $category = \core_course_category::get($coursea->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); $this->setUser($user); // Viewing the category calendar for a category the user doesn't have any enrolments in. // Should see that category, and all categories underneath it. $calendar = \calendar_information::create(time(), SITEID, $wrongcategory->id); $this->assertCount(1, $calendar->courses); $this->assertCount(4, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertEquals(\context_coursecat::instance($wrongcategory->id), $calendar->context); } /** * Given a user has an enrolment in two courses in the separate category. * And I ask for the site informatino. * Then I should see the site. * And I should see both course. * And I should see the categories that my enrolled courses are in. * And I should see the parents of those categories. */ public function test_site_visibility_two_courses_two_categories_site(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['B.1']; $categorya = \core_course_category::get($coursea->category); $categoryb = \core_course_category::get($courseb->category); $wrongcategory = $categories['C']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); $this->setUser($user); // Viewing the site azs a whole. // Should see all courses that this user is enrolled in. $calendar = \calendar_information::create(time(), SITEID, null); $this->assertCount(3, $calendar->courses); $this->assertCount(2, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertArrayHasKey($coursea->id, array_flip($calendar->courses)); $this->assertArrayHasKey($courseb->id, array_flip($calendar->courses)); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertEquals(\context_system::instance(), $calendar->context); } /** * Given a user has an enrolment in two courses in the separate category. * And I ask for the course information for one of those courses. * Then I should see the site. * And I should see one of the courses. * And I should see the categories that my enrolled courses are in. * And I should see the parents of those categories. */ public function test_site_visibility_two_courses_two_categories_coursea(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['B.1']; $categorya = \core_course_category::get($coursea->category); $categoryb = \core_course_category::get($courseb->category); $wrongcategory = $categories['C']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); $this->setUser($user); $time = time(); // Viewing the course calendar. // Should see all courses that this user is enrolled in. $calendar = \calendar_information::create($time, $coursea->id, null); $this->assertCount(2, $calendar->courses); $this->assertCount(1, $calendar->categories); $this->assertEquals($coursea->id, $calendar->courseid); $this->assertArrayHasKey($coursea->id, array_flip($calendar->courses)); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertEquals(\context_course::instance($coursea->id), $calendar->context); // Viewing the course calendar while specifying the categorya too. // The categorya is essentially ignored. No change expected. $calendarwithcategory = \calendar_information::create($time, $coursea->id, $categorya->id); $this->assertEquals($calendar, $calendarwithcategory); // Viewing the course calendar while specifying the wrong categorya. // The categorya is essentially ignored. No change expected. $calendarwithwrongcategory = \calendar_information::create($time, $coursea->id, $wrongcategory->id); $this->assertEquals($calendar, $calendarwithwrongcategory); } /** * Given a user has an enrolment in two courses in the separate category. * And I ask for the course information for the second of those courses. * Then I should see the site. * And I should see one of the courses. * And I should see the categories that my enrolled courses are in. * And I should see the parents of those categories. */ public function test_site_visibility_two_courses_two_categories_courseb(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['B.1']; $categorya = \core_course_category::get($coursea->category); $categoryb = \core_course_category::get($courseb->category); $wrongcategory = $categories['C']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); $this->setUser($user); $time = time(); // Viewing the other course calendar. // Should see all courses that this user is enrolled in. $calendar = \calendar_information::create($time, $courseb->id, null); $this->assertCount(2, $calendar->courses); $this->assertCount(1, $calendar->categories); $this->assertEquals($courseb->id, $calendar->courseid); $this->assertArrayHasKey($courseb->id, array_flip($calendar->courses)); $this->assertArrayNotHasKey($coursea->id, array_flip($calendar->courses)); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertEquals(\context_course::instance($courseb->id), $calendar->context); // Viewing the other course calendar while specifying the categorya too. // The categorya is essentially ignored. No change expected. $calendarwithcategory = \calendar_information::create($time, $courseb->id, $categoryb->id); $this->assertEquals($calendar, $calendarwithcategory); // Viewing the other course calendar while specifying the wrong categorya. // The categorya is essentially ignored. No change expected. $calendarwithwrongcategory = \calendar_information::create($time, $courseb->id, $wrongcategory->id); $this->assertEquals($calendar, $calendarwithwrongcategory); } /** * Given a user has an enrolment in two courses in separate categories. * And I ask for the category information. * Then I should see the site. * And I should see one of the courses. * And I should see the categories that my enrolled courses are in. * And I should see the parents of those categories. */ public function test_site_visibility_two_courses_two_categories_category(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['B.1']; $categorya = \core_course_category::get($coursea->category); $categoryb = \core_course_category::get($courseb->category); $wrongcategory = $categories['C']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); $this->setUser($user); // Viewing the categorya calendar. // Should see all courses that this user is enrolled in. $calendar = \calendar_information::create(time(), SITEID, $categorya->id); $this->assertCount(2, $calendar->courses); $this->assertCount(7, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertArrayHasKey($coursea->id, array_flip($calendar->courses)); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertArrayNotHasKey($courseb->id, array_flip($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1i']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1ii']->id, array_flip($calendar->categories)); $this->assertEquals(\context_coursecat::instance($categorya->id), $calendar->context); } /** * Given a user has an enrolment in two courses in the separate category. * And I ask for the category information of a different category. * Then I should see the site. * And I should see one of the courses. * And I should see the categories that my enrolled courses are in. * And I should see the parents of those categories. */ public function test_site_visibility_two_courses_two_categories_different_category(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['B.1']; $categorya = \core_course_category::get($coursea->category); $categoryb = \core_course_category::get($courseb->category); $wrongcategory = $categories['C']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); $this->setUser($user); // Viewing the categorya calendar for a categorya the user doesn't have any enrolments in. // Should see that categorya, and all categories underneath it. $calendar = \calendar_information::create(time(), SITEID, $wrongcategory->id); $this->assertCount(1, $calendar->courses); $this->assertCount(1, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertEquals(\context_coursecat::instance($wrongcategory->id), $calendar->context); } /** * Given an admin user with no enrolments. * And I ask for the site information. * Then I should see the site. * And I should see no other courses. * And I should see no categories. */ public function test_site_visibility_admin_user(): void { $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $this->setAdminUser(); $calendar = \calendar_information::create(time(), SITEID, null); $this->assertCount(1, $calendar->courses); $this->assertCount(0, $calendar->categories); $this->assertEquals(SITEID, $calendar->courseid); $this->assertEquals(SITEID, reset($calendar->courses)); $this->assertEquals(\context_system::instance(), $calendar->context); } /** * Given an admin user with a single enrolments. * And I ask for the site information. * Then I should see the site. * And I should see the course I am enrolled in * And I should see the category of that course. * And I should see the parents of that course category. * And I should see no other courses. * And I should see no other categories. */ public function test_site_visibility_admin_user_with_enrolment_site(): void { global $USER; $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $course = $courses['A1.1']; $category = \core_course_category::get($course->category); $this->setAdminUser(); $generator->enrol_user($USER->id, $course->id); $calendar = \calendar_information::create(time(), SITEID, null); $this->assertCount(2, $calendar->courses); $this->assertCount(2, $calendar->categories); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertArrayHasKey($course->id, array_flip($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertEquals(\context_system::instance(), $calendar->context); } /** * Given an admin user with a single enrolments. * And I ask for the course information. * Then I should see the site. * And I should see the course I am enrolled in * And I should see the category of that course. * And I should see the parents of that course category. */ public function test_site_visibility_admin_user_with_enrolment_course(): void { global $USER; $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $course = $courses['A1.1']; $category = \core_course_category::get($course->category); $wrongcategory = $categories['B1']; $this->setAdminUser(); $generator->enrol_user($USER->id, $course->id); $time = time(); $calendar = \calendar_information::create($time, $course->id, null); $this->assertCount(2, $calendar->courses); $this->assertCount(2, $calendar->categories); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertArrayHasKey($course->id, array_flip($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertEquals(\context_course::instance($course->id), $calendar->context); // Viewing the course calendar while specifying the category too. // The category is essentially ignored. No change expected. $calendarwithcategory = \calendar_information::create($time, $course->id, $category->id); $this->assertEquals($calendar, $calendarwithcategory); // Viewing the course calendar while specifying the wrong category. // The category is essentially ignored. No change expected. $calendarwithwrongcategory = \calendar_information::create($time, $course->id, $wrongcategory->id); $this->assertEquals($calendar, $calendarwithwrongcategory); } /** * Given an admin user with a single enrolments. * And I ask for the category information for the category my course is in. * Then I should see that category. * And I should see the category parents. * And I should see the category descendants. * And I should see the site. * And I should see my course. * And I should see no other courses. * And I should see no categories. */ public function test_site_visibility_admin_user_with_enrolment_category(): void { global $USER; $this->resetAfterTest(); list ($courses, $categories) = $this->mock_structure(); $generator = $this->getDataGenerator(); $course = $courses['A1.1']; $category = \core_course_category::get($course->category); $wrongcategory = $categories['B1']; $this->setAdminUser(); $generator->enrol_user($USER->id, $course->id); $calendar = \calendar_information::create(time(), SITEID, $category->id); $this->assertCount(2, $calendar->courses); $this->assertCount(4, $calendar->categories); $this->assertArrayHasKey(SITEID, array_flip($calendar->courses)); $this->assertArrayHasKey($course->id, array_flip($calendar->courses)); $this->assertArrayHasKey($categories['A']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1i']->id, array_flip($calendar->categories)); $this->assertArrayHasKey($categories['A1ii']->id, array_flip($calendar->categories)); $this->assertEquals(\context_coursecat::instance($category->id), $calendar->context); } } PK*\!X tests/event_times_test.phpnu[. namespace core_calendar; use core_calendar\local\event\value_objects\event_times; /** * Event times tests. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class event_times_test extends \advanced_testcase { /** * Test event times class getters. * * @dataProvider getters_testcases * @param array $constructorparams Associative array of constructor parameters. */ public function test_getters($constructorparams): void { $eventtimes = new event_times( $constructorparams['start_time'], $constructorparams['end_time'], $constructorparams['sort_time'], $constructorparams['modified_time'], $constructorparams['usermidnight_time'] ); foreach ($constructorparams as $name => $value) { $this->assertEquals($eventtimes->{'get_' . $name}(), $value); } $this->assertEquals($eventtimes->get_duration(), $constructorparams['end_time']->diff($constructorparams['start_time'])); } /** * Test cases for getters test. */ public static function getters_testcases(): array { return [ 'Dataset 1' => [ 'constructorparams' => [ 'start_time' => (new \DateTimeImmutable())->setTimestamp(-386380800), 'end_time' => (new \DateTimeImmutable())->setTimestamp(115776000), 'sort_time' => (new \DateTimeImmutable())->setTimestamp(115776000), 'modified_time' => (new \DateTimeImmutable())->setTimestamp(time()), 'usermidnight_time' => (new \DateTimeImmutable())->setTimestamp(115776000), ] ], 'Dataset 2' => [ 'constructorparams' => [ 'start_time' => (new \DateTimeImmutable())->setTimestamp(123456), 'end_time' => (new \DateTimeImmutable())->setTimestamp(12345678), 'sort_time' => (new \DateTimeImmutable())->setTimestamp(1111), 'modified_time' => (new \DateTimeImmutable())->setTimestamp(time()), 'usermidnight_time' => (new \DateTimeImmutable())->setTimestamp(1111), ] ] ]; } } PK*\}tests/action_event_test.phpnu[. /** * Action event tests. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar; use core_calendar\local\event\entities\action_event; use core_calendar\local\event\value_objects\action; 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_collection_interface; use core_calendar\local\event\entities\event_interface; defined('MOODLE_INTERNAL') || die; /** * Action event testcase. * * @package core_calendar * @category test * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class action_event_test extends \advanced_testcase { /** * Test event class getters. * * @dataProvider getters_testcases * @param array $constructorparams Associative array of constructor parameters. */ public function test_getters($constructorparams): void { $event = new action_event( $constructorparams['event'], $constructorparams['action'] ); foreach ($constructorparams as $name => $value) { if ($name !== 'event') { $this->assertEquals($event->{'get_' . $name}(), $value); } } } /** * Test cases for getters test. */ public static function getters_testcases(): array { return [ 'Dataset 1' => [ 'constructorparams' => [ 'event' => new core_calendar_action_event_test_event(), 'action' => new action( 'action 1', new \moodle_url('http://example.com'), 2, true ) ] ], 'Dataset 2' => [ 'constructorparams' => [ 'event' => new core_calendar_action_event_test_event(), 'action' => new action( 'action 2', new \moodle_url('http://example.com'), 5, false ) ] ], ]; } } /** * Test event. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_calendar_action_event_test_event implements event_interface { public function get_id() { return 1729; } public function get_name() { return 'Jeff'; } public function get_description() { return new event_description('asdf', 1); } public function get_location() { return 'Cube office'; } public function get_category() { return new \stdClass(); } public function get_course() { return new \stdClass(); } public function get_course_module() { return new \stdClass(); } public function get_group() { return new \stdClass(); } public function get_user() { return new \stdClass(); } public function get_type() { return 'asdf'; } public function get_times() { return new event_times( (new \DateTimeImmutable())->setTimestamp(-386380800), (new \DateTimeImmutable())->setTimestamp(115776000), (new \DateTimeImmutable())->setTimestamp(115776000), (new \DateTimeImmutable())->setTimestamp(time()) ); } public function get_repeats() { return new core_calendar_action_event_test_event_collection(); } public function get_subscription() { return new \stdClass(); } public function is_visible() { return true; } /** * Component * @return string|null */ public function get_component() { return null; } } /** * Test event collection. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_calendar_action_event_test_event_collection implements event_collection_interface { /** * @var array */ protected $events; /** * core_calendar_action_event_test_event_collection constructor. */ public function __construct() { $this->events = [ 'not really an event hahaha', 'also not really. gottem.' ]; } public function get_id() { return 1729; } public function get_num() { return 2; } public function getIterator(): \Traversable { foreach ($this->events as $event) { yield $event; } } } PK*\ 4``tests/coursecat_proxy_test.phpnu[. namespace core_calendar; use core_calendar\local\event\proxies\coursecat_proxy; /** * coursecat_proxy testcase. * * @package core_calendar * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class coursecat_proxy_test extends \advanced_testcase { public function test_valid_coursecat(): void { global $DB; $this->resetAfterTest(); $name = '2027-2028 Academic Year'; $generator = $this->getDataGenerator(); $category = $generator->create_category([ 'name' => $name, ]); \cache_helper::purge_by_event('changesincoursecat'); // Fetch the proxy. $startreads = $DB->perf_get_reads(); $proxy = new coursecat_proxy($category->id); $this->assertInstanceOf(coursecat_proxy::class, $proxy); $this->assertEquals(0, $DB->perf_get_reads() - $startreads); // Fetch the ID - this is known and doesn't require a cache read. $this->assertEquals($category->id, $proxy->get('id')); $this->assertEquals(0, $DB->perf_get_reads() - $startreads); // Fetch the name - not known, and requires a read. $this->assertEquals($name, $proxy->get('name')); $this->assertEquals(1, $DB->perf_get_reads() - $startreads); $this->assertInstanceOf('core_course_category', $proxy->get_proxied_instance()); } } PK*\Eܡ/%/%#tests/calendartype_test_example.phpnu[. namespace calendartype_test_example; use \core_calendar\type_base; /** * Handles calendar functions for the test calendar. * * The test calendar is going to be 2 years, 2 days, 2 hours and 2 minutes * in the future of the Gregorian calendar. * * @package core_calendar * @copyright 2013 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class structure extends type_base { /** * Returns the name of the calendar. * * @return string the calendar name */ public function get_name() { return 'test_example'; } /** * Returns a list of all the possible days for all months. * * This is used to generate the select box for the days * in the date selector elements. Some months contain more days * than others so this function should return all possible days as * we can not predict what month will be chosen (the user * may have JS turned off and we need to support this situation in * Moodle). * * @return array the days */ public function get_days() { $days = array(); for ($i = 1; $i <= 31; $i++) { $days[$i] = $i; } return $days; } /** * Returns a list of all the names of the months. * * @return array the month names */ public function get_months() { $months = array(); for ($i = 1; $i <= 12; $i++) { $months[$i] = $i; } return $months; } /** * Returns the minimum year for the calendar. * * @return int The minimum year */ public function get_min_year() { return 1900; } /** * Returns the maximum year for the calendar * * @return int The maximum year */ public function get_max_year() { return 2050; } /** * Returns an array of years. * * @param int $minyear * @param int $maxyear * @return array the years. */ public function get_years($minyear = null, $maxyear = null) { if (is_null($minyear)) { $minyear = $this->get_min_year(); } if (is_null($maxyear)) { $maxyear = $this->get_max_year(); } $years = array(); for ($i = $minyear; $i <= $maxyear; $i++) { $years[$i] = $i; } return $years; } /** * Returns a multidimensional array with information for day, month, year * and the order they are displayed when selecting a date. * The order in the array will be the order displayed when selecting a date. * Override this function to change the date selector order. * * @param int $minyear The year to start with. * @param int $maxyear The year to finish with. * @return array Full date information. */ public function get_date_order($minyear = null, $maxyear = null) { $dateinfo = array(); $dateinfo['day'] = $this->get_days(); $dateinfo['month'] = $this->get_months(); $dateinfo['year'] = $this->get_years($minyear, $maxyear); return $dateinfo; } /** * Returns the number of days in a week. * * @return int the number of days */ public function get_num_weekdays() { return 7; } /** * Returns an indexed list of all the names of the weekdays. * * The list starts with the index 0. Each index, representing a * day, must be an array that contains the indexes 'shortname' * and 'fullname'. * * @return array array of days */ public function get_weekdays() { return ''; } /** * Returns the index of the starting week day. * * @return int */ public function get_starting_weekday() { return ''; } /** * Returns the index of the weekday for a specific calendar date. * * @param int $year * @param int $month * @param int $day * @return int */ public function get_weekday($year, $month, $day) { return ''; } /** * Returns the number of days in a given month. * * @param int $year * @param int $month * @return int the number of days */ public function get_num_days_in_month($year, $month) { return ''; } /** * Get the previous month. * * If the current month is January, it will get the last month of the previous year. * * @param int $year * @param int $month * @return array previous month and year */ public function get_prev_month($year, $month) { return ''; } /** * Get the next month. * * If the current month is December, it will get the first month of the following year. * * @param int $year * @param int $month * @return array the following month and year */ public function get_next_month($year, $month) { return ''; } /** * Returns a formatted string that represents a date in user time. * * @param int $time the timestamp in UTC, as obtained from the database * @param string $format strftime format * @param int|float|string $timezone the timezone to use * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} * @param bool $fixday if true then the leading zero from %d is removed, * if false then the leading zero is maintained * @param bool $fixhour if true then the leading zero from %I is removed, * if false then the leading zero is maintained * @return string the formatted date/time */ public function timestamp_to_date_string($time, $format, $timezone, $fixday, $fixhour) { return ''; } /** * Given a $time timestamp in GMT (seconds since epoch), returns an array that represents * the date in user time. * * @param int $time timestamp in GMT * @param float|int|string $timezone the timezone to use to calculate the time * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} * @return array an array that represents the date in user time */ public function timestamp_to_date_array($time, $timezone = 99) { $gregoriancalendar = \core_calendar\type_factory::get_calendar_instance('gregorian'); $date = $gregoriancalendar->timestamp_to_date_array($time, $timezone); $newdate = $this->convert_from_gregorian($date['year'], $date['mon'], $date['mday'], $date['hours'], $date['minutes']); $date['year'] = $newdate['year']; $date['mon'] = $newdate['month']; $date['mday'] = $newdate['day']; $date['hours'] = $newdate['hour']; $date['minutes'] = $newdate['minute']; return $date; } /** * Provided with a day, month, year, hour and minute * convert it into the equivalent Gregorian date. * * @param int $year * @param int $month * @param int $day * @param int $hour * @param int $minute * @return array the converted day, month, year, hour and minute. */ public function convert_to_gregorian($year, $month, $day, $hour = 0, $minute = 0) { $timestamp = make_timestamp($year, $month, $day, $hour, $minute); $date = date('Y/n/j/H/i', strtotime('-2 year, -2 months, -2 days, -2 hours, -2 minutes', $timestamp)); list($year, $month, $day, $hour, $minute) = explode('/', $date); return array('year' => (int) $year, 'month' => (int) $month, 'day' => (int) $day, 'hour' => (int) $hour, 'minute' => (int) $minute); } /** * Provided with a day, month, year, hour and minute in a Gregorian date * convert it into the specific calendar type date. * * @param int $year * @param int $month * @param int $day * @param int $hour * @param int $minute * @return array the converted day, month, year, hour and minute. */ public function convert_from_gregorian($year, $month, $day, $hour = 0, $minute = 0) { $timestamp = make_timestamp($year, $month, $day, $hour, $minute); $date = date('Y/n/j/H/i', strtotime('+2 year, +2 months, +2 days, +2 hours, +2 minutes', $timestamp)); list($year, $month, $day, $hour, $minute) = explode('/', $date); return array('year' => (int) $year, 'month' => (int) $month, 'day' => (int) $day, 'hour' => (int) $hour, 'minute' => (int) $minute); } /** * This return locale for windows os. * * @return string locale */ public function locale_win_charset() { return get_string('localewincharset', 'langconfig'); } } PK*\:44tests/calendartype_test.phpnu[. /** * This file contains the class that handles testing the calendar type system. * * @package core_calendar * @copyright 2013 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar; defined('MOODLE_INTERNAL') || die(); global $CFG; // The test calendar type. require_once($CFG->dirroot . '/calendar/tests/calendartype_test_example.php'); // Used to test the dateselector elements. require_once($CFG->libdir . '/form/dateselector.php'); require_once($CFG->libdir . '/form/datetimeselector.php'); // Used to test the user datetime profile field. require_once($CFG->dirroot . '/user/profile/lib.php'); require_once($CFG->dirroot . '/user/profile/definelib.php'); /** * Unit tests for the calendar type system. * * @package core_calendar * @copyright 2013 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.6 */ final class calendartype_test extends \advanced_testcase { /** @var MoodleQuickForm Keeps reference of dummy form object */ private $mform; /** * The test user. */ private $user; /** * Test set up. */ protected function setUp(): void { parent::setUp(); // The user we are going to test this on. $this->user = self::getDataGenerator()->create_user(); self::setUser($this->user); // Get form data. $form = new temp_form_calendartype(); $this->mform = $form->getform(); } /** * Test that setting the calendar type works. */ public function test_calendar_type_set(): void { // We want to reset the test data after this run. $this->resetAfterTest(); // Test setting it as the 'Test' calendar type. $this->set_calendar_type('test_example'); $this->assertEquals('test_example', \core_calendar\type_factory::get_calendar_type()); // Test setting it as the 'Gregorian' calendar type. $this->set_calendar_type('gregorian'); $this->assertEquals('gregorian', \core_calendar\type_factory::get_calendar_type()); } /** * Test that calling core Moodle functions responsible for displaying the date * have the same results as directly calling the same function in the calendar type. */ public function test_calendar_type_core_functions(): void { // We want to reset the test data after this run. $this->resetAfterTest(); // Test that the core functions reproduce the same results as the Gregorian calendar. $this->core_functions_test('gregorian'); // Test that the core functions reproduce the same results as the test calendar. $this->core_functions_test('test_example'); } /** * Test that dates selected using the date selector elements are being saved as unixtime, and that the * unixtime is being converted back to a valid date to display in the date selector elements for * different calendar types. */ public function test_calendar_type_dateselector_elements(): void { // We want to reset the test data after this run. $this->resetAfterTest(); $this->setTimezone('UTC'); // Note: this test is pretty useless because it does not test current user timezones. // Check converting dates to Gregorian when submitting a date selector element works. Note: the test // calendar is 2 years, 2 months, 2 days, 2 hours and 2 minutes ahead of the Gregorian calendar. $date1 = array(); $date1['day'] = 4; $date1['month'] = 7; $date1['year'] = 2013; $date1['hour'] = 0; $date1['minute'] = 0; $date1['timestamp'] = 1372896000; $this->convert_dateselector_to_unixtime_test('dateselector', 'gregorian', $date1); $date2 = array(); $date2['day'] = 7; $date2['month'] = 9; $date2['year'] = 2015; $date2['hour'] = 0; // The dateselector element does not have hours. $date2['minute'] = 0; // The dateselector element does not have minutes. $date2['timestamp'] = 1372896000; $this->convert_dateselector_to_unixtime_test('dateselector', 'test_example', $date2); $date3 = array(); $date3['day'] = 4; $date3['month'] = 7; $date3['year'] = 2013; $date3['hour'] = 23; $date3['minute'] = 15; $date3['timestamp'] = 1372979700; $this->convert_dateselector_to_unixtime_test('datetimeselector', 'gregorian', $date3); $date4 = array(); $date4['day'] = 7; $date4['month'] = 9; $date4['year'] = 2015; $date4['hour'] = 1; $date4['minute'] = 17; $date4['timestamp'] = 1372979700; $this->convert_dateselector_to_unixtime_test('datetimeselector', 'test_example', $date4); // The date selector element values are set by using the function usergetdate, here we want to check that // the unixtime passed is being successfully converted to the correct values for the calendar type. $this->convert_unixtime_to_dateselector_test('gregorian', $date3); $this->convert_unixtime_to_dateselector_test('test_example', $date4); } /** * Test that the user profile field datetime minimum and maximum year settings are saved as the * equivalent Gregorian years. */ public function test_calendar_type_datetime_field_submission(): void { // We want to reset the test data after this run. $this->resetAfterTest(); // Create an array with the input values and expected values once submitted. $date = array(); $date['inputminyear'] = '1970'; $date['inputmaxyear'] = '2013'; $date['expectedminyear'] = '1970'; $date['expectedmaxyear'] = '2013'; $this->datetime_field_submission_test('gregorian', $date); // The test calendar is 2 years, 2 months, 2 days in the future, so when the year 1970 is submitted, // the year 1967 should be saved in the DB, as 1/1/1970 converts to 30/10/1967 in Gregorian. $date['expectedminyear'] = '1967'; $date['expectedmaxyear'] = '2010'; $this->datetime_field_submission_test('test_example', $date); } /** * Test all the core functions that use the calendar type system. * * @param string $type the calendar type we want to test */ private function core_functions_test($type) { $this->set_calendar_type($type); // Get the calendar. $calendar = \core_calendar\type_factory::get_calendar_instance(); // Test the userdate function. $this->assertEquals($calendar->timestamp_to_date_string($this->user->timecreated, '', 99, true, true), userdate($this->user->timecreated)); // Test the userdate function with a timezone. $this->assertEquals( $calendar->timestamp_to_date_string($this->user->timecreated, '', 'Australia/Sydney', true, true), userdate($this->user->timecreated, timezone: 'Australia/Sydney'), ); // Test the calendar/lib.php functions. $this->assertEquals($calendar->get_weekdays(), calendar_get_days()); $this->assertEquals($calendar->get_starting_weekday(), calendar_get_starting_weekday()); $this->assertEquals($calendar->get_num_days_in_month('1986', '9'), calendar_days_in_month('9', '1986')); $this->assertEquals($calendar->get_next_month('1986', '9'), calendar_add_month('9', '1986')); $this->assertDebuggingCalled( 'Deprecation: calendar_add_month has been deprecated since 5.0. ' . 'Use \core_calendar\type_factory::get_calendar_instance()->get_next_month() instead. ' . 'See MDL-84657 for more information.' ); $this->assertEquals($calendar->get_prev_month('1986', '9'), calendar_sub_month('9', '1986')); $this->assertDebuggingCalled( 'Deprecation: calendar_sub_month has been deprecated since 5.0. ' . 'Use \core_calendar\type_factory::get_calendar_instance()->get_prev_month() instead. ' . 'See MDL-79434 for more information.' ); // Test the lib/moodle.php functions. $this->assertEquals($calendar->get_num_days_in_month('1986', '9'), days_in_month('9', '1986')); $this->assertEquals($calendar->get_weekday('1986', '9', '16'), dayofweek('16', '9', '1986')); } /** * Simulates submitting a form with a date selector element and tests that the chosen dates * are converted into unixtime before being saved in DB. * * @param string $element the form element we are testing * @param string $type the calendar type we want to test * @param array $date the date variables */ private function convert_dateselector_to_unixtime_test($element, $type, $date) { $this->set_calendar_type($type); static $counter = 0; $counter++; if ($element == 'dateselector') { $el = $this->mform->addElement('date_selector', 'dateselector' . $counter, null, array('timezone' => 0.0)); } else { $el = $this->mform->addElement('date_time_selector', 'dateselector' . $counter, null, array('timezone' => 0.0, 'optional' => false)); } $submitvalues = array('dateselector' . $counter => $date); $this->assertSame(array('dateselector' . $counter => $date['timestamp']), $el->exportValue($submitvalues, true)); } /** * Test converting dates from unixtime to a date for the calendar type specified. * * @param string $type the calendar type we want to test * @param array $date the date variables */ private function convert_unixtime_to_dateselector_test($type, $date) { $this->set_calendar_type($type); // Get the calendar. $calendar = \core_calendar\type_factory::get_calendar_instance(); $usergetdate = $calendar->timestamp_to_date_array($date['timestamp'], 0.0); $comparedate = array( 'minute' => $usergetdate['minutes'], 'hour' => $usergetdate['hours'], 'day' => $usergetdate['mday'], 'month' => $usergetdate['mon'], 'year' => $usergetdate['year'], 'timestamp' => $date['timestamp'] ); $this->assertEquals($comparedate, $date); } /** * Test saving the minimum and max year settings for the user datetime field. * * @param string $type the calendar type we want to test * @param array $date the date variables */ private function datetime_field_submission_test($type, $date) { $this->set_calendar_type($type); // Get the data we are submitting for the form. $formdata = array(); $formdata['id'] = 0; $formdata['shortname'] = 'Shortname'; $formdata['name'] = 'Name'; $formdata['param1'] = $date['inputminyear']; $formdata['param2'] = $date['inputmaxyear']; $formdata['datatype'] = 'datetime'; // Mock submitting this. \core_user\form\profile_field_form::mock_submit($formdata); // Create the user datetime form. $form = new \core_user\form\profile_field_form(); // Get the data from the submission. $submissiondata = $form->get_data(); // On the user profile field page after get_data, the function define_save is called // in the field base class, which then calls the field's function define_save_preprocess. $field = new \profile_define_datetime(); $submissiondata = $field->define_save_preprocess($submissiondata); // Create an array we want to compare with the date passed. $comparedate = $date; $comparedate['expectedminyear'] = $submissiondata->param1; $comparedate['expectedmaxyear'] = $submissiondata->param2; $this->assertEquals($comparedate, $date); } /** * Set the calendar type for this user. * * @param string $type the calendar type we want to set */ private function set_calendar_type($type) { $this->user->calendartype = $type; \core\session\manager::set_user($this->user); } } /** * Form object to be used in test case. */ class temp_form_calendartype extends \moodleform { /** * Form definition. */ public function definition() { // No definition required. } /** * Returns form reference * @return MoodleQuickForm */ public function getform() { $mform = $this->_form; // Set submitted flag, to simulate submission. $mform->_flagSubmitted = true; return $mform; } } PK*\ BBB+tests/raw_event_retrieval_strategy_test.phpnu[. namespace core_calendar; use core_calendar\local\event\strategies\raw_event_retrieval_strategy; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/calendar/tests/helpers.php'); /** * Raw event retrieval strategy tests. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class raw_event_retrieval_strategy_test extends \advanced_testcase { /** * Test retrieval strategy when module is disabled. */ public function test_get_raw_events_with_disabled_module(): void { global $DB; $this->resetAfterTest(); $retrievalstrategy = new raw_event_retrieval_strategy(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $student = $generator->create_user(); $generator->enrol_user($student->id, $course->id, 'student'); $this->setUser($student); $events = [ [ 'name' => 'Start of assignment', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'assign', 'instance' => 1, 'eventtype' => 'due', 'timestart' => time(), 'timeduration' => 86400, 'visible' => 1 ], [ 'name' => 'Start of lesson', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'lesson', 'instance' => 1, 'eventtype' => 'end', 'timestart' => time(), 'timeduration' => 86400, 'visible' => 1 ] ]; foreach ($events as $event) { \calendar_event::create($event, false); } // Get all events. $events = $retrievalstrategy->get_raw_events(null, [0], null); $this->assertCount(2, $events); // Disable the lesson module. $DB->set_field('modules', 'visible', 0, ['name' => 'lesson']); // Check that we only return the assign event. $events = $retrievalstrategy->get_raw_events(null, [0], null); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('assign', $event->modulename); // Now, log out and repeat the above test in the reverse order. $this->setUser(); // Check that we only return the assign event (given that the lesson module is still disabled). $events = $retrievalstrategy->get_raw_events([$student->id], [0], null); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('assign', $event->modulename); // Enable the lesson module. $DB->set_field('modules', 'visible', 1, ['name' => 'lesson']); // Get all events. $events = $retrievalstrategy->get_raw_events(null, [0], null); $this->assertCount(2, $events); } /** * Test retrieval strategy when there are overrides. */ public function test_get_raw_event_strategy_with_overrides(): void { $this->resetAfterTest(); $retrievalstrategy = new raw_event_retrieval_strategy(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $instance = $plugingenerator->create_instance(['course' => $course->id]); // Create users. $useroverridestudent = $generator->create_user(); $group1student = $generator->create_user(); $group2student = $generator->create_user(); $group12student = $generator->create_user(); $nogroupstudent = $generator->create_user(); // Enrol users. $generator->enrol_user($useroverridestudent->id, $course->id, 'student'); $generator->enrol_user($group1student->id, $course->id, 'student'); $generator->enrol_user($group2student->id, $course->id, 'student'); $generator->enrol_user($group12student->id, $course->id, 'student'); $generator->enrol_user($nogroupstudent->id, $course->id, 'student'); // Create groups. $group1 = $generator->create_group(['courseid' => $course->id, 'name' => 'Group 1']); $group2 = $generator->create_group(['courseid' => $course->id, 'name' => 'Group 2']); // Add members to groups. $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group1student->id]); $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group2student->id]); $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group12student->id]); $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group12student->id]); $now = time(); // Events with the same module name, instance and event type. $events = [ [ 'name' => 'Assignment 1 due date', 'description' => '', 'location' => 'Test', 'format' => 0, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'due', 'timestart' => $now, 'timeduration' => 0, 'visible' => 1 ], [ 'name' => 'Assignment 1 due date - User override', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => 0, 'groupid' => 0, 'userid' => $useroverridestudent->id, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'due', 'timestart' => $now + 86400, 'timeduration' => 0, 'visible' => 1, 'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY ], [ 'name' => 'Assignment 1 due date - Group A override', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => $course->id, 'groupid' => $group1->id, 'userid' => 2, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'due', 'timestart' => $now + (2 * 86400), 'timeduration' => 0, 'visible' => 1, 'priority' => 1, ], [ 'name' => 'Assignment 1 due date - Group B override', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => $course->id, 'groupid' => $group2->id, 'userid' => 2, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'due', 'timestart' => $now + (3 * 86400), 'timeduration' => 0, 'visible' => 1, 'priority' => 2, ], ]; foreach ($events as $event) { \calendar_event::create($event, false); } $groups = [$group1->id, $group2->id]; // Do the following tests multiple times when logged in with different users. Also run the whole set when logged out. // In any cases, the tests should not depend on the logged-in user. foreach ([$useroverridestudent, $nogroupstudent, $group12student, $group1student, null] as $login) { $this->setUser($login); // Get user override events. $events = $retrievalstrategy->get_raw_events([$useroverridestudent->id], $groups, [$course->id]); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date - User override', $event->name); // Get events for user that does not belong to any group and has no user override events. $events = $retrievalstrategy->get_raw_events([$nogroupstudent->id], $groups, [$course->id]); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date', $event->name); // Get events for user that belongs to groups A and B and has no user override events. $events = $retrievalstrategy->get_raw_events([$group12student->id], $groups, [$course->id]); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date - Group A override', $event->name); // Get events for user that belongs to group A and has no user override events. $events = $retrievalstrategy->get_raw_events([$group1student->id], $groups, [$course->id]); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date - Group A override', $event->name); } // Add repeating events. $repeatingevents = [ [ 'name' => 'Repeating site event', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => SITEID, 'groupid' => 0, 'userid' => 2, 'repeatid' => 1, 'modulename' => '0', 'instance' => 0, 'eventtype' => 'site', 'timestart' => $now + 86400, 'timeduration' => 0, 'visible' => 1, ], [ 'name' => 'Repeating site event', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => SITEID, 'groupid' => 0, 'userid' => 2, 'repeatid' => 1, 'modulename' => '0', 'instance' => 0, 'eventtype' => 'site', 'timestart' => $now + (2 * 86400), 'timeduration' => 0, 'visible' => 1, ], ]; foreach ($repeatingevents as $event) { \calendar_event::create($event, false); } // Make sure repeating events are not filtered out. $events = $retrievalstrategy->get_raw_events(); $this->assertCount(3, $events); } /** * Test retrieval strategy with category specifications. */ public function test_get_raw_events_category(): void { $this->resetAfterTest(); $retrievalstrategy = new raw_event_retrieval_strategy(); $generator = $this->getDataGenerator(); $category1 = $generator->create_category(); $category2 = $generator->create_category(); $events = [ [ 'name' => 'E1', 'eventtype' => 'category', 'description' => '', 'location' => 'Test', 'format' => 1, 'categoryid' => $category1->id, 'userid' => 2, 'timestart' => time(), ], [ 'name' => 'E2', 'eventtype' => 'category', 'description' => '', 'location' => 'Test', 'format' => 1, 'categoryid' => $category2->id, 'userid' => 2, 'timestart' => time() + 1, ], ]; foreach ($events as $event) { \calendar_event::create($event, false); } // Get all events. $events = $retrievalstrategy->get_raw_events(null, null, null, null); $this->assertCount(2, $events); $event = array_shift($events); $this->assertEquals('E1', $event->name); $event = array_shift($events); $this->assertEquals('E2', $event->name); // Get events for C1 events. $events = $retrievalstrategy->get_raw_events(null, null, null, [$category1->id]); $this->assertCount(1, $events); $event = array_shift($events); $this->assertEquals('E1', $event->name); // Get events for C2 events. $events = $retrievalstrategy->get_raw_events(null, null, null, [$category2->id]); $this->assertCount(1, $events); $event = array_shift($events); $this->assertEquals('E2', $event->name); // Get events for several categories. $events = $retrievalstrategy->get_raw_events(null, null, null, [$category1->id, $category2->id]); $this->assertCount(2, $events); } public function test_get_raw_events_for_multiple_users(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); // Create users. $user1 = $generator->create_user(); $user2 = $generator->create_user(); $user3 = $generator->create_user(); // Create user events. $events = [ [ 'name' => 'User1 Event', 'eventtype' => 'user', 'userid' => $user1->id, 'timestart' => time(), ], [ 'name' => 'User2 Event', 'eventtype' => 'user', 'userid' => $user2->id, 'timestart' => time(), ], [ 'name' => 'User3 Event', 'eventtype' => 'user', 'userid' => $user3->id, 'timestart' => time(), ] ]; foreach ($events as $event) { \calendar_event::create($event, false); } $retrievalstrategy = new raw_event_retrieval_strategy(); // Get all events. $events = $retrievalstrategy->get_raw_events([$user1->id, $user2->id]); $this->assertCount(2, $events); $this->assertEqualsCanonicalizing( ['User1 Event', 'User2 Event'], array_column($events, 'name')); } public function test_get_raw_events_for_groups_with_no_members(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); // Create groups. $group1 = $generator->create_group(['courseid' => $course->id, 'name' => 'Group 1']); $group2 = $generator->create_group(['courseid' => $course->id, 'name' => 'Group 2']); // Create group events. $events = [ [ 'name' => 'Group 1 Event', 'eventtype' => 'group', 'groupid' => $group1->id, 'timestart' => time(), ], [ 'name' => 'Group 2 Event', 'eventtype' => 'group', 'groupid' => $group2->id, 'timestart' => time(), ] ]; foreach ($events as $event) { \calendar_event::create($event, false); } $retrievalstrategy = new raw_event_retrieval_strategy; // Get group eventsl. $events = $retrievalstrategy->get_raw_events(null, [$group1->id, $group2->id]); $this->assertCount(2, $events); $this->assertEqualsCanonicalizing( ['Group 1 Event', 'Group 2 Event'], array_column($events, 'name')); } /** * Test retrieval strategy with empty filters. * This covers a edge case not covered elsewhere to ensure its SQL is cross * db compatible. The test is ensuring we don't get a DML Exception with * the filters setup this way. */ public function test_get_raw_events_with_empty_user_and_category_lists(): void { $retrievalstrategy = new raw_event_retrieval_strategy; $retrievalstrategy->get_raw_events([], null, null, []); } } PK*\T %tests/output/humantimeperiod_test.phpnu[. namespace core_calendar\output; use DateTime; /** * Tests for humantimeperiod_test class. * * @covers \core_calendar\output\humantimeperiod * @package core_calendar * @category test * @copyright 2025 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class humantimeperiod_test extends \advanced_testcase { /** * Test format_period() method. * * @dataProvider provider_format_period * @param int|null $addsecondsend The number of seconds to add to the current time for the end date. * @param bool $expectedendnull Whether the end date is null. */ public function test_format_period( ?int $addsecondsend, bool $expectedendnull, ): void { $this->resetAfterTest(); // 26 February 2025 15:30:00 (GMT). $clock = $this->mock_clock_with_frozen(1740583800); $timestampstart = $clock->time(); $timestampend = is_null($addsecondsend) ? null : $clock->time() + $addsecondsend; $humandateend = null; if (!$expectedendnull) { $humandateend = humandate::create_from_timestamp( timestamp: $timestampend, timeonly: (abs($addsecondsend) < 1800), ); } $expected = [ 'startdate' => humandate::create_from_timestamp($timestampstart), 'enddate' => $humandateend, ]; $humantimeperiod = humantimeperiod::create_from_timestamp($timestampstart, $timestampend); $reflection = new \ReflectionClass($humantimeperiod); $method = $reflection->getMethod('format_period'); $method->setAccessible(true); $result = $method->invoke($humantimeperiod); $this->compare_output($expected, $result); } /** * Data provider. * * @return array */ public static function provider_format_period(): array { return [ 'Same start and end' => [ 'addsecondsend' => 0, 'expectedendnull' => true, ], 'Null end date' => [ 'addsecondsend' => null, 'expectedendnull' => true, ], '1 day ahead' => [ 'addsecondsend' => 87000, 'expectedendnull' => false, ], '1 day behind' => [ 'addsecondsend' => -87000, 'expectedendnull' => false, ], '29 minutes ahead' => [ 'addsecondsend' => 1799, 'expectedendnull' => false, ], '30 minutes ahead (Midnight)' => [ 'addsecondsend' => 1800, 'expectedendnull' => false, ], '31 minutes ahead (Midnight)' => [ 'addsecondsend' => 1801, 'expectedendnull' => false, ], ]; } /** * Test create_from_timestamp. */ public function test_create_from_timestamp(): void { $this->resetAfterTest(); $clock = \core\di::get(\core\clock::class); $timestamp = $clock->time(); $humantimeperiod = humantimeperiod::create_from_timestamp($timestamp, $timestamp + 3600); $this->assertInstanceOf(humantimeperiod::class, $humantimeperiod); } /** * Test create_from_timestamp with enddatetime null. */ public function test_create_from_timestamp_endnull(): void { $this->resetAfterTest(); $clock = $this->mock_clock_with_frozen(); $timestamp = $clock->time(); $humantimeperiod = humantimeperiod::create_from_timestamp($timestamp, null); $this->assertInstanceOf(humantimeperiod::class, $humantimeperiod); } /** * Test create_from_datetime. */ public function test_create_from_datetime(): void { $this->resetAfterTest(); $humantimeperiod = humantimeperiod::create_from_datetime(new DateTime(), new DateTime('+1 hour')); $this->assertInstanceOf(humantimeperiod::class, $humantimeperiod); } /** * Test create_from_datetime with enddatetime null. */ public function test_create_from_datetime_endnull(): void { $this->resetAfterTest(); $humantimeperiod = humantimeperiod::create_from_datetime(new DateTime(), null); $this->assertInstanceOf(humantimeperiod::class, $humantimeperiod); } /** * Compare humantimeperiod output. * * @param array $expected The expected output. * @param array $actual The actual output. */ protected function compare_output( array $expected, array $actual, ): void { $fields = ['startdate', 'enddate']; foreach ($fields as $field) { $this->assertEquals($expected[$field], $actual[$field], "Field $field does not match"); } } } PK*\c2#2#tests/output/humandate_test.phpnu[. namespace core_calendar\output; use DateTime; /** * Tests for humandate class. * * @covers \core_calendar\output\humandate * @package core_calendar * @category test * @copyright 2025 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class humandate_test extends \advanced_testcase { /** * Initialize. */ protected function setUp(): void { parent::setUp(); // Mock the clock. $this->setTimezone('Australia/Perth'); } /** * Test export_for_template() method. * * @dataProvider provider_export_for_template * @param int $addseconds The number of seconds to add to the current time. * @param bool $userelatives Whether to use relative dates. * @param string|null $date For relative dates, the expected string (Tomorrow, Today, Yesterday). * @param bool $ispast Whether the date is in the past. * @param bool $needtitle Whether the date needs a title. * @param bool $isnear Whether the date is near. * @param string $userdateformat The user date expected format. */ public function test_export_for_template( int $addseconds, bool $userelatives, ?string $date, bool $ispast, bool $needtitle, bool $isnear, string $userdateformat, ): void { global $PAGE; $this->resetAfterTest(); // 26 February 2025 15:59:59 (GMT). $clock = $this->mock_clock_with_frozen(1740585599); $renderer = $PAGE->get_renderer('core'); $timestamp = $clock->time() + $addseconds; $expected = [ 'timestamp' => $timestamp, 'date' => $date, 'userdate' => userdate($timestamp, get_string($userdateformat)), 'ispast' => $ispast, 'needtitle' => $needtitle, 'isnear' => $isnear, ]; $humandate = humandate::create_from_timestamp($timestamp); $humandate->set_use_relatives($userelatives); $result = $humandate->export_for_template($renderer); $this->compare_output($expected, $result, $userelatives); } /** * Data provider. * * @return array */ public static function provider_export_for_template(): array { return [ 'Now with relatives' => [ 'addseconds' => 0, 'userelatives' => true, 'date' => 'Today', 'ispast' => false, 'needtitle' => true, 'isnear' => false, 'userdateformat' => 'strftimedayshort', ], 'Tomorrow with relatives' => [ 'addseconds' => DAYSECS, 'userelatives' => true, 'date' => 'Tomorrow', 'ispast' => false, 'needtitle' => true, 'isnear' => false, 'userdateformat' => 'strftimedayshort', ], 'Yesterday with relatives' => [ 'addseconds' => -DAYSECS, 'userelatives' => true, 'date' => 'Yesterday', 'ispast' => true, 'needtitle' => true, 'isnear' => false, 'userdateformat' => 'strftimedayshort', ], 'One hour future with relatives' => [ 'addseconds' => HOURSECS, 'userelatives' => true, 'date' => 'Tomorrow', 'ispast' => false, 'needtitle' => true, 'isnear' => true, 'userdateformat' => 'strftimedayshort', ], 'One hour past with relatives' => [ 'addseconds' => -HOURSECS, 'userelatives' => true, 'date' => 'Today', 'ispast' => true, 'needtitle' => true, 'isnear' => false, 'userdateformat' => 'strftimedayshort', ], 'Now without relatives' => [ 'addseconds' => 0, 'userelatives' => false, 'date' => 'Today', 'ispast' => false, 'needtitle' => false, 'isnear' => false, 'userdateformat' => 'strftimedayshort', ], 'Tomorrow without relatives' => [ 'addseconds' => DAYSECS, 'userelatives' => false, 'date' => 'Tomorrow', 'ispast' => false, 'needtitle' => false, 'isnear' => false, 'userdateformat' => 'strftimedayshort', ], 'Yesterday without relatives' => [ 'addseconds' => -DAYSECS, 'userelatives' => false, 'date' => 'Yesterday', 'ispast' => true, 'needtitle' => false, 'isnear' => false, 'userdateformat' => 'strftimedayshort', ], 'One hour future without relatives' => [ 'addseconds' => HOURSECS, 'userelatives' => false, 'date' => null, 'ispast' => false, 'needtitle' => false, 'isnear' => true, 'userdateformat' => 'strftimedayshort', ], 'One hour past without relatives' => [ 'addseconds' => -HOURSECS, 'userelatives' => false, 'date' => null, 'ispast' => true, 'needtitle' => false, 'isnear' => false, 'userdateformat' => 'strftimedayshort', ], 'one year from now' => [ 'addseconds' => YEARSECS, 'userelatives' => false, 'date' => null, 'ispast' => false, 'needtitle' => false, 'isnear' => false, 'userdateformat' => 'strftimedaydate', ], 'one year in the past' => [ 'addseconds' => -YEARSECS, 'userelatives' => false, 'date' => null, 'ispast' => true, 'needtitle' => false, 'isnear' => false, 'userdateformat' => 'strftimedaydate', ], ]; } public function test_create_from_timestamp(): void { $this->resetAfterTest(); $clock = $this->mock_clock_with_frozen(); $timestamp = $clock->time(); $humandate = humandate::create_from_timestamp($timestamp); $this->assertInstanceOf(humandate::class, $humandate); } public function test_create_from_datetime(): void { $this->resetAfterTest(); $humandate = humandate::create_from_datetime(new DateTime()); $this->assertInstanceOf(humandate::class, $humandate); } /** * Compare humandate output. * * @param array $expected The expected output. * @param array $actual The actual output. * @param bool $userelatives Whether to use relative dates. */ protected function compare_output( array $expected, array $actual, bool $userelatives, ): void { $fields = ['timestamp', 'userdate', 'ispast', 'needtitle']; foreach ($fields as $field) { $this->assertEquals($expected[$field], $actual[$field], "Field $field does not match"); } if ($expected['isnear']) { $this->assertEquals($expected[$field], $actual[$field], "Field isnear does not match"); } else { $this->assertArrayNotHasKey('isnear', $actual); } if (!is_null($expected['date'])) { if ($userelatives) { $this->assertStringContainsString($expected['date'], $actual['date']); } else { $this->assertStringNotContainsString($expected['date'], $actual['date']); } } else { $this->assertStringNotContainsString('Today', $actual['date']); $this->assertStringNotContainsString('Yesterday', $actual['date']); $this->assertStringNotContainsString('Tomorrow', $actual['date']); } } } PK*\,+tests/events_related_objects_cache_test.phpnu[. namespace core_calendar; use core_calendar\external\events_related_objects_cache; use core_calendar\local\event\container; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/helpers.php'); /** * Tests for the events_related_objects_cache. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class events_related_objects_cache_test extends \advanced_testcase { /** * Tests set up */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * An event with no module should return null when trying to retrieve * the module instance. */ public function test_get_module_instance_no_module(): void { $this->setAdminUser(); $mapper = container::get_event_mapper(); $legacyevent = create_event([ 'modulename' => '', 'instance' => 0 ]); $event = $mapper->from_legacy_event_to_event($legacyevent); $cache = new events_related_objects_cache([$event]); $this->assertNull($cache->get_module_instance($event)); } /** * The get_module_instance should return the correct module instances * for the given set of events in the cache. */ public function test_get_module_instance_with_modules(): void { $this->setAdminUser(); $mapper = container::get_event_mapper(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $plugingenerator = $generator->get_plugin_generator('mod_assign'); $instance1 = $plugingenerator->create_instance(['course' => $course->id]); $instance2 = $plugingenerator->create_instance(['course' => $course->id]); unset($instance1->cmid); unset($instance2->cmid); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course->id, 'modulename' => 'assign', 'userid' => 0, 'eventtype' => 'due', 'repeats' => 0, 'timestart' => 1, ]; $legacyevent1 = create_event(array_merge($params, ['name' => 'Event 1', 'instance' => $instance1->id])); $legacyevent2 = create_event(array_merge($params, ['name' => 'Event 2', 'instance' => $instance1->id])); $legacyevent3 = create_event(array_merge($params, ['name' => 'Event 3', 'instance' => $instance2->id])); $event1 = $mapper->from_legacy_event_to_event($legacyevent1); $event2 = $mapper->from_legacy_event_to_event($legacyevent2); $event3 = $mapper->from_legacy_event_to_event($legacyevent3); $cache = new events_related_objects_cache([$event1, $event2, $event3]); $eventinstance1 = $cache->get_module_instance($event1); $eventinstance2 = $cache->get_module_instance($event2); $eventinstance3 = $cache->get_module_instance($event3); $this->assertEquals($instance1, $eventinstance1); $this->assertEquals($instance1, $eventinstance2); $this->assertEquals($instance2, $eventinstance3); } /** * Trying to load the course module of an event that isn't in * the cache should return null. */ public function test_module_instance_unknown_event(): void { $this->setAdminUser(); $mapper = container::get_event_mapper(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $plugingenerator = $generator->get_plugin_generator('mod_assign'); $instance1 = $plugingenerator->create_instance(['course' => $course->id]); $instance2 = $plugingenerator->create_instance(['course' => $course->id]); unset($instance1->cmid); unset($instance2->cmid); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course->id, 'modulename' => 'assign', 'userid' => 0, 'eventtype' => 'due', 'repeats' => 0, 'timestart' => 1, ]; $legacyevent1 = create_event(array_merge($params, ['name' => 'Event 1', 'instance' => $instance1->id])); $legacyevent2 = create_event(array_merge($params, ['name' => 'Event 2', 'instance' => $instance2->id])); $event1 = $mapper->from_legacy_event_to_event($legacyevent1); $event2 = $mapper->from_legacy_event_to_event($legacyevent2); $cache = new events_related_objects_cache([$event1]); $this->assertNull($cache->get_module_instance($event2)); } } PK*\55tests/externallib_test.phpnu[. namespace core_calendar; use core_calendar_external; use externallib_advanced_testcase; use core_external\external_api; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * External course functions unit tests * * @package core_calendar * @category external * @copyright 2012 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.5 */ final class externallib_test extends externallib_advanced_testcase { /** * Tests set up */ protected function setUp(): void { global $CFG; require_once($CFG->dirroot . '/calendar/externallib.php'); parent::setUp(); } /** Create calendar events or update them * Set $prop->id, if you want to do an update instead of creating an new event * * @param string $name Event title * @param int $userid User id * @param string $type Event type * @param int $repeats Number of repeated events to create * @param int $timestart Time stamp of the event start * @param mixed $prop List of event properties as array or object * @return mixed Event object or false; * @since Moodle 2.5 */ public static function create_calendar_event($name, $userid = 0, $type = 'user', $repeats = 0, $timestart = null, $prop = null) { global $CFG, $DB, $SITE; require_once("$CFG->dirroot/calendar/lib.php"); if (!empty($prop)) { if (is_array($prop)) { $prop = (object)$prop; } } else { $prop = new \stdClass(); } $prop->name = $name; if (empty($prop->eventtype)) { $prop->eventtype = $type; } if (empty($prop->repeats)) { $prop->repeats = $repeats; } if (empty($prop->timestart)) { $prop->timestart = time(); } if (empty($prop->timeduration)) { $prop->timeduration = 0; } if (empty($prop->timesort)) { $prop->timesort = 0; } if (empty($prop->type)) { $prop->type = CALENDAR_EVENT_TYPE_STANDARD; } if (empty($prop->repeats)) { $prop->repeat = 0; } else { $prop->repeat = 1; } if (empty($prop->userid)) { if (!empty($userid)) { $prop->userid = $userid; } else { $prop->userid = 0; } } if (!isset($prop->courseid)) { // Set a default value of the event's course ID field. if ($type === 'user') { // If it's a user event, course ID should be zero. $prop->courseid = 0; } else { // Otherwise, default to the site ID. $prop->courseid = $SITE->id; } } // Determine event priority. if ($prop->courseid == 0 && isset($prop->groupid) && $prop->groupid == 0 && !empty($prop->userid)) { // User override event. $prop->priority = CALENDAR_EVENT_USER_OVERRIDE_PRIORITY; } else if ($prop->courseid != $SITE->id && !empty($prop->groupid)) { // Group override event. $priorityparams = ['courseid' => $prop->courseid, 'groupid' => $prop->groupid]; // Group override event with the highest priority. $groupevents = $DB->get_records('event', $priorityparams, 'priority DESC', 'id, priority', 0, 1); $priority = 1; if (!empty($groupevents)) { $event = reset($groupevents); if (!empty($event->priority)) { $priority = $event->priority + 1; } } $prop->priority = $priority; } $event = new \calendar_event($prop); return $event->create($prop); } public function test_create_calendar_events(): void { global $DB, $USER; $this->setAdminUser(); $this->resetAfterTest(); $prevcount = count($DB->get_records("event")); // Create a few events and do asserts. $this->create_calendar_event('test', $USER->id); $where = $DB->sql_compare_text('name') ." = ?"; $count = count($DB->get_records_select("event", $where, array('test'))); $this->assertEquals(1, $count); $aftercount = count($DB->get_records("event")); $this->assertEquals($prevcount + 1, $aftercount); $this->create_calendar_event('user', $USER->id, 'user', 3); $where = $DB->sql_compare_text('name') ." = ?"; $count = count($DB->get_records_select("event", $where, array('user'))); $this->assertEquals(3, $count); $aftercount = count($DB->get_records("event")); $this->assertEquals($prevcount + 4, $aftercount); } /** * Test delete_calendar_events */ public function test_delete_calendar_events(): void { global $DB, $USER; $this->resetAfterTest(true); $this->setAdminUser(); // Create a few stuff to test with. $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $record = new \stdClass(); $record->courseid = $course->id; $group = $this->getDataGenerator()->create_group($record); $notdeletedcount = $DB->count_records('event'); // Let's create a few events. $siteevent = $this->create_calendar_event('site', $USER->id, 'site'); $record = new \stdClass(); $record->courseid = $course->id; $courseevent = $this->create_calendar_event('course', $USER->id, 'course', 2, time(), $record); $userevent = $this->create_calendar_event('user', $USER->id); $record = new \stdClass(); $record->courseid = $course->id; $record->groupid = $group->id; $groupevent = $this->create_calendar_event('group', $USER->id, 'group', 0, time(), $record); // Now lets try to delete stuff with proper rights. $events = array( array('eventid' => $siteevent->id, 'repeat' => 0), array('eventid' => $courseevent->id, 'repeat' => 1), array('eventid' => $userevent->id, 'repeat' => 0), array('eventid' => $groupevent->id, 'repeat' => 0) ); core_calendar_external::delete_calendar_events($events); // Check to see if things were deleted properly. $deletedcount = $DB->count_records('event'); $this->assertEquals($notdeletedcount, $deletedcount); // Let's create a few events. $siteevent = $this->create_calendar_event('site', $USER->id, 'site'); $record = new \stdClass(); $record->courseid = $course->id; $courseevent = $this->create_calendar_event('course', $USER->id, 'course', 3, time(), $record); $userevent = $this->create_calendar_event('user', $user->id); $record = new \stdClass(); $record->courseid = $course->id; $record->groupid = $group->id; $groupevent = $this->create_calendar_event('group', $USER->id, 'group', 0, time(), $record); $this->setuser($user); $sitecontext = \context_system::instance(); $coursecontext = \context_course::instance($course->id); $usercontext = \context_user::instance($user->id); $role = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id); // Remove all caps. $this->unassignUserCapability('moodle/calendar:manageentries', $sitecontext->id, $role->id); $this->unassignUserCapability('moodle/calendar:manageentries', $coursecontext->id, $role->id); $this->unassignUserCapability('moodle/calendar:managegroupentries', $coursecontext->id, $role->id); $this->unassignUserCapability('moodle/calendar:manageownentries', $usercontext->id, $role->id); // Assign proper caps and attempt delete. $this->assignUserCapability('moodle/calendar:manageentries', $sitecontext->id, $role->id); $events = array( array('eventid' => $siteevent->id, 'repeat' => 0), ); core_calendar_external::delete_calendar_events($events); $deletedcount = $DB->count_records('event'); $count = $notdeletedcount+5; $this->assertEquals($count, $deletedcount); $this->assignUserCapability('moodle/calendar:manageentries', $sitecontext->id, $role->id); $events = array( array('eventid' => $courseevent->id, 'repeat' => 0), ); core_calendar_external::delete_calendar_events($events); $deletedcount = $DB->count_records('event'); $count = $notdeletedcount+4; $this->assertEquals($count, $deletedcount); $this->assignUserCapability('moodle/calendar:manageownentries', $usercontext->id, $role->id); $events = array( array('eventid' => $userevent->id, 'repeat' => 0), ); core_calendar_external::delete_calendar_events($events); $deletedcount = $DB->count_records('event'); $count = $notdeletedcount+3; $this->assertEquals($count, $deletedcount); $this->assignUserCapability('moodle/calendar:managegroupentries', $coursecontext->id, $role->id); $events = array( array('eventid' => $groupevent->id, 'repeat' => 0), ); core_calendar_external::delete_calendar_events($events); $deletedcount = $DB->count_records('event'); $count = $notdeletedcount+2; $this->assertEquals($count, $deletedcount); $notdeletedcount = $deletedcount; // Let us try deleting without caps. $siteevent = $this->create_calendar_event('site', $USER->id, 'site'); $record = new \stdClass(); $record->courseid = $course->id; $courseevent = $this->create_calendar_event('course', $USER->id, 'course', 3, time(), $record); $userevent = $this->create_calendar_event('user', $USER->id); $record = new \stdClass(); $record->courseid = $course->id; $record->groupid = $group->id; $groupevent = $this->create_calendar_event('group', $USER->id, 'group', 0, time(), $record); $this->setGuestUser(); $events = array( array('eventid' => $siteevent->id, 'repeat' => 0), array('eventid' => $courseevent->id, 'repeat' => 0), array('eventid' => $userevent->id, 'repeat' => 0), array('eventid' => $groupevent->id, 'repeat' => 0) ); $this->expectException(\moodle_exception::class); core_calendar_external::delete_calendar_events($events); } /** * Test get_calendar_events */ public function test_get_calendar_events(): void { global $DB, $USER; $this->resetAfterTest(true); set_config('calendar_adminseesall', 1); $this->setAdminUser(); // Create a few stuff to test with. $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $category = $this->getDataGenerator()->create_category(); $category2 = $this->getDataGenerator()->create_category(); $category2b = $this->getDataGenerator()->create_category(['parent' => $category2->id]); $course3 = $this->getDataGenerator()->create_course(['category' => $category2b->id]); $role = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user2->id, $course3->id, $role->id); $record = new \stdClass(); $record->courseid = $course->id; $group = $this->getDataGenerator()->create_group($record); $beforecount = $DB->count_records('event'); // Let's create a few events. $siteevent = $this->create_calendar_event('site', $USER->id, 'site'); // This event will have description with an inline fake image. $draftidfile = file_get_unused_draft_itemid(); $usercontext = \context_course::instance($course->id); $filerecord = array( 'contextid' => $usercontext->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => $draftidfile, 'filepath' => '/', 'filename' => 'fakeimage.png', ); $fs = get_file_storage(); $fs->create_file_from_string($filerecord, 'img contents'); $record = new \stdClass(); $record->courseid = $course->id; $record->groupid = 0; $record->description = array( 'format' => FORMAT_HTML, 'text' => 'Text with img ', 'itemid' => $draftidfile ); $courseevent = $this->create_calendar_event('course', $USER->id, 'course', 2, time(), $record); $record = new \stdClass(); $record->courseid = 0; $record->groupid = 0; $userevent = $this->create_calendar_event('user', $USER->id, 'user', 0, time(), $record); $record = new \stdClass(); $record->courseid = $course->id; $record->groupid = $group->id; $groupevent = $this->create_calendar_event('group', $USER->id, 'group', 0, time(), $record); $paramevents = array ('eventids' => array($siteevent->id), 'courseids' => array($course->id), 'groupids' => array($group->id), 'categoryids' => array($category->id)); $options = array ('siteevents' => true, 'userevents' => true); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); // Check to see if we got all events. $this->assertEquals(5, count($events['events'])); $this->assertEquals(0, count($events['warnings'])); $options = array ('siteevents' => true, 'userevents' => true, 'timeend' => time() + 7*WEEKSECS); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(5, count($events['events'])); $this->assertEquals(0, count($events['warnings'])); // Expect the same URL in the description of two different events (because they are repeated). $coursecontext = \context_course::instance($course->id); $expectedurl = "webservice/pluginfile.php/$coursecontext->id/calendar/event_description/$courseevent->id/fakeimage.png"; $withdescription = 0; foreach ($events['events'] as $event) { if (!empty($event['description'])) { $withdescription++; $this->assertStringContainsString($expectedurl, $event['description']); } } $this->assertEquals(2, $withdescription); // Let's play around with caps. // Create user event for the user $user. $record = new \stdClass(); $record->courseid = 0; $record->groupid = 0; $this->create_calendar_event('user', $user->id, 'user', 0, time(), $record); $this->setUser($user); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(2, count($events['events'])); // site, user. $this->assertEquals(2, count($events['warnings'])); // course, group. $role = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(4, count($events['events'])); // site, user, both course events. $this->assertEquals(1, count($events['warnings'])); // group. $options = array ('siteevents' => true, 'userevents' => true, 'timeend' => time() + HOURSECS); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(3, count($events['events'])); // site, user, one course event. $this->assertEquals(1, count($events['warnings'])); // group. groups_add_member($group, $user); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(4, count($events['events'])); // site, user, group, one course event. $this->assertEquals(0, count($events['warnings'])); $paramevents = array ('courseids' => array($course->id), 'groupids' => array($group->id)); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(4, count($events['events'])); // site, user, group, one course event. $this->assertEquals(0, count($events['warnings'])); $paramevents = array ('groupids' => array($group->id, 23)); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(3, count($events['events'])); // site, user, group. $this->assertEquals(1, count($events['warnings'])); $paramevents = array ('courseids' => array(23)); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(2, count($events['events'])); // site, user. $this->assertEquals(1, count($events['warnings'])); $paramevents = array (); $options = array ('siteevents' => false, 'userevents' => false, 'timeend' => time() + 7*WEEKSECS); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(0, count($events['events'])); // nothing returned. $this->assertEquals(0, count($events['warnings'])); $paramevents = array ('eventids' => array($siteevent->id, $groupevent->id)); $options = array ('siteevents' => false, 'userevents' => false, 'timeend' => time() + 7*WEEKSECS); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(2, count($events['events'])); // site, group. $this->assertEquals(0, count($events['warnings'])); $paramevents = array ('eventids' => array($siteevent->id)); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(1, count($events['events'])); // site. $this->assertEquals(0, count($events['warnings'])); // Try getting a course event by its id. $paramevents = array ('eventids' => array($courseevent->id)); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(1, count($events['events'])); $this->assertEquals(0, count($events['warnings'])); // Now, create an activity event. $this->setAdminUser(); $nexttime = time() + DAYSECS; $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id, 'duedate' => $nexttime)); $this->setUser($user); $paramevents = array ('courseids' => array($course->id)); $options = array ('siteevents' => true, 'userevents' => true, 'timeend' => time() + WEEKSECS); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertCount(5, $events['events']); // Hide the assignment. set_coursemodule_visible($assign->cmid, 0); // Empty all the caches that may be affected by this change. accesslib_clear_all_caches_for_unit_testing(); \course_modinfo::clear_instance_cache(); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); // Expect one less. $this->assertCount(4, $events['events']); // Create some category events. $this->setAdminUser(); $record = new \stdClass(); $record->courseid = 0; $record->categoryid = $category->id; $record->timestart = time() - DAYSECS; $catevent1 = $this->create_calendar_event('category a', $USER->id, 'category', 0, time(), $record); $record = new \stdClass(); $record->courseid = 0; $record->categoryid = $category2->id; $record->timestart = time() + DAYSECS; $catevent2 = $this->create_calendar_event('category b', $USER->id, 'category', 0, time(), $record); // Now as student, make sure we get the events of the courses I am enrolled. $this->setUser($user2); $paramevents = array('categoryids' => array($category2b->id)); $options = array('timeend' => time() + 7 * WEEKSECS, 'userevents' => false, 'siteevents' => false); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); // Should be just one, since there's just one category event of the course I am enrolled (course3 - cat2b). $this->assertEquals(1, count($events['events'])); $this->assertEquals($catevent2->id, $events['events'][0]['id']); $this->assertEquals($category2->id, $events['events'][0]['categoryid']); $this->assertEquals(0, count($events['warnings'])); // Now get category events but by course (there aren't course events in the course). $paramevents = array('courseids' => array($course3->id)); $options = array('timeend' => time() + 7 * WEEKSECS, 'userevents' => false, 'siteevents' => false); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(1, count($events['events'])); $this->assertEquals($catevent2->id, $events['events'][0]['id']); $this->assertEquals(0, count($events['warnings'])); // Empty events in one where I'm not enrolled and one parent category // (parent of a category where this is a course where the user is enrolled). $paramevents = array('categoryids' => array($category2->id, $category->id)); $options = array('timeend' => time() + 7 * WEEKSECS, 'userevents' => false, 'siteevents' => false); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(1, count($events['events'])); $this->assertEquals($catevent2->id, $events['events'][0]['id']); $this->assertEquals(0, count($events['warnings'])); // Admin can see all category events. $this->setAdminUser(); $paramevents = array('categoryids' => array($category->id, $category2->id, $category2b->id)); $options = array('timeend' => time() + 7 * WEEKSECS, 'userevents' => false, 'siteevents' => false); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(2, count($events['events'])); $this->assertEquals(0, count($events['warnings'])); $this->assertEquals($catevent1->id, $events['events'][0]['id']); $this->assertEquals($category->id, $events['events'][0]['categoryid']); $this->assertEquals($catevent2->id, $events['events'][1]['id']); $this->assertEquals($category2->id, $events['events'][1]['categoryid']); } /** * Test get_calendar_events with mathjax in the name. */ public function test_get_calendar_events_with_mathjax(): void { global $USER; $this->resetAfterTest(true); set_config('calendar_adminseesall', 1); $this->setAdminUser(); // Enable MathJax filter in content and headings. $this->configure_filters([ ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true], ]); // Create a site event with mathjax in the name and description. $siteevent = $this->create_calendar_event('Site Event $$(a+b)=2$$', $USER->id, 'site', 0, time(), ['description' => 'Site Event Description $$(a+b)=2$$']); // Now call the WebService. $events = core_calendar_external::get_calendar_events(); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); // Format the original data. $sitecontext = \context_system::instance(); $siteevent->name = $siteevent->format_external_name(); list($siteevent->description, $siteevent->descriptionformat) = $siteevent->format_external_text(); // Check that the event data is formatted. $this->assertCount(1, $events['events']); $this->assertStringContainsString('', $events['events'][0]['name']); $this->assertStringContainsString('', $events['events'][0]['description']); $this->assertEquals($siteevent->name, $events['events'][0]['name']); $this->assertEquals($siteevent->description, $events['events'][0]['description']); } /** * Test core_calendar_external::create_calendar_events */ public function test_core_create_calendar_events(): void { global $DB, $USER, $SITE; $this->resetAfterTest(true); $this->setAdminUser(); // Create a few stuff to test with. $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $record = new \stdClass(); $record->courseid = $course->id; $group = $this->getDataGenerator()->create_group($record); $prevcount = $DB->count_records('event'); // Let's create a few events. $events = array ( array('name' => 'site', 'courseid' => $SITE->id, 'eventtype' => 'site'), array('name' => 'course', 'courseid' => $course->id, 'eventtype' => 'course', 'repeats' => 2), array('name' => 'group', 'courseid' => $course->id, 'groupid' => $group->id, 'eventtype' => 'group'), array('name' => 'user') ); $eventsret = core_calendar_external::create_calendar_events($events); $eventsret = external_api::clean_returnvalue(core_calendar_external::create_calendar_events_returns(), $eventsret); // Check to see if things were created properly. $aftercount = $DB->count_records('event'); $this->assertEquals($prevcount + 5, $aftercount); $this->assertEquals(5, count($eventsret['events'])); $this->assertEquals(0, count($eventsret['warnings'])); $sitecontext = \context_system::instance(); $coursecontext = \context_course::instance($course->id); $this->setUser($user); $prevcount = $aftercount; $events = array ( array('name' => 'course', 'courseid' => $course->id, 'eventtype' => 'course', 'repeats' => 2), array('name' => 'group', 'courseid' => $course->id, 'groupid' => $group->id, 'eventtype' => 'group'), array('name' => 'user') ); $role = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id); groups_add_member($group, $user); $this->assignUserCapability('moodle/calendar:manageentries', $coursecontext->id, $role->id); $this->assignUserCapability('moodle/calendar:managegroupentries', $coursecontext->id, $role->id); $eventsret = core_calendar_external::create_calendar_events($events); $eventsret = external_api::clean_returnvalue(core_calendar_external::create_calendar_events_returns(), $eventsret); // Check to see if things were created properly. $aftercount = $DB->count_records('event'); $this->assertEquals($prevcount + 4, $aftercount); $this->assertEquals(4, count($eventsret['events'])); $this->assertEquals(0, count($eventsret['warnings'])); // Check to see nothing was created without proper permission. $this->setGuestUser(); $prevcount = $DB->count_records('event'); $eventsret = core_calendar_external::create_calendar_events($events); $eventsret = external_api::clean_returnvalue(core_calendar_external::create_calendar_events_returns(), $eventsret); $aftercount = $DB->count_records('event'); $this->assertEquals($prevcount, $aftercount); $this->assertEquals(0, count($eventsret['events'])); $this->assertEquals(3, count($eventsret['warnings'])); $this->setUser($user); $this->unassignUserCapability('moodle/calendar:manageentries', $coursecontext->id, $role->id); $this->unassignUserCapability('moodle/calendar:managegroupentries', $coursecontext->id, $role->id); $prevcount = $DB->count_records('event'); $eventsret = core_calendar_external::create_calendar_events($events); $eventsret = external_api::clean_returnvalue(core_calendar_external::create_calendar_events_returns(), $eventsret); $aftercount = $DB->count_records('event'); $this->assertEquals($prevcount + 1, $aftercount); // User event. $this->assertEquals(1, count($eventsret['events'])); $this->assertEquals(2, count($eventsret['warnings'])); } /** * Requesting calendar events from a given time should return all events with a sort * time at or after the requested time. All events prior to that time should not * be return. * * If there are no events on or after the given time then an empty result set should * be returned. */ public function test_get_calendar_action_events_by_timesort_after_time(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->resetAfterTest(true); $this->setUser($user); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'modulename' => 'assign', 'instance' => $moduleinstance->id, 'courseid' => $course->id, ]; $event1 = $this->create_calendar_event('Event 1', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 1])); $event2 = $this->create_calendar_event('Event 2', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 2])); $event3 = $this->create_calendar_event('Event 3', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 3])); $event4 = $this->create_calendar_event('Event 4', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 4])); $event5 = $this->create_calendar_event('Event 5', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 5])); $event6 = $this->create_calendar_event('Event 6', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 6])); $event7 = $this->create_calendar_event('Event 7', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 7])); $event8 = $this->create_calendar_event('Event 8', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 8])); $result = core_calendar_external::get_calendar_action_events_by_timesort(5); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_timesort_returns(), $result ); $events = $result['events']; $this->assertCount(4, $events); $this->assertEquals('Event 5', $events[0]['name']); $this->assertEquals('Event 6', $events[1]['name']); $this->assertEquals('Event 7', $events[2]['name']); $this->assertEquals('Event 8', $events[3]['name']); $this->assertEquals($event5->id, $result['firstid']); $this->assertEquals($event8->id, $result['lastid']); $result = core_calendar_external::get_calendar_action_events_by_timesort(9); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_timesort_returns(), $result ); $this->assertEmpty($result['events']); $this->assertNull($result['firstid']); $this->assertNull($result['lastid']); // Requesting action events on behalf of another user. $this->setAdminUser(); $result = core_calendar_external::get_calendar_action_events_by_timesort(5, null, 0, 20, false, $user->id); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_timesort_returns(), $result ); $events = $result['events']; $this->assertCount(4, $events); $this->assertEquals('Event 5', $events[0]['name']); $this->assertEquals('Event 6', $events[1]['name']); $this->assertEquals('Event 7', $events[2]['name']); $this->assertEquals('Event 8', $events[3]['name']); $this->assertEquals($event5->id, $result['firstid']); $this->assertEquals($event8->id, $result['lastid']); } /** * Requesting calendar events before a given time should return all events with a sort * time at or before the requested time (inclusive). All events after that time * should not be returned. * * If there are no events before the given time then an empty result set should be * returned. */ public function test_get_calendar_action_events_by_timesort_before_time(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->resetAfterTest(true); $this->setUser($user); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'modulename' => 'assign', 'instance' => $moduleinstance->id, 'courseid' => $course->id, ]; $event1 = $this->create_calendar_event('Event 1', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 2])); $event2 = $this->create_calendar_event('Event 2', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 3])); $event3 = $this->create_calendar_event('Event 3', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 4])); $event4 = $this->create_calendar_event('Event 4', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 5])); $event5 = $this->create_calendar_event('Event 5', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 6])); $event6 = $this->create_calendar_event('Event 6', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 7])); $event7 = $this->create_calendar_event('Event 7', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 8])); $event8 = $this->create_calendar_event('Event 8', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 9])); $result = core_calendar_external::get_calendar_action_events_by_timesort(null, 5); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_timesort_returns(), $result ); $events = $result['events']; $this->assertCount(4, $events); $this->assertEquals('Event 1', $events[0]['name']); $this->assertEquals('Event 2', $events[1]['name']); $this->assertEquals('Event 3', $events[2]['name']); $this->assertEquals('Event 4', $events[3]['name']); $this->assertEquals($event1->id, $result['firstid']); $this->assertEquals($event4->id, $result['lastid']); $result = core_calendar_external::get_calendar_action_events_by_timesort(null, 1); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_timesort_returns(), $result ); $this->assertEmpty($result['events']); $this->assertNull($result['firstid']); $this->assertNull($result['lastid']); // Requesting action events on behalf of another user. $this->setAdminUser(); $result = core_calendar_external::get_calendar_action_events_by_timesort(null, 5, 0, 20, false, $user->id); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_timesort_returns(), $result ); $events = $result['events']; $this->assertCount(4, $events); $this->assertEquals('Event 1', $events[0]['name']); $this->assertEquals('Event 2', $events[1]['name']); $this->assertEquals('Event 3', $events[2]['name']); $this->assertEquals('Event 4', $events[3]['name']); $this->assertEquals($event1->id, $result['firstid']); $this->assertEquals($event4->id, $result['lastid']); } /** * Test retrieving event that was overridden for a user */ public function test_get_calendar_events_override(): void { $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $teacher = $this->getDataGenerator()->create_user(); $anotheruser = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); $this->resetAfterTest(true); $this->setAdminUser(); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'modulename' => 'assign', 'instance' => $moduleinstance->id, ]; $now = time(); // Create two events - one for everybody in the course and one only for the first student. $event1 = $this->create_calendar_event('Base event', 0, 'due', 0, $now + DAYSECS, $params + ['courseid' => $course->id]); $event2 = $this->create_calendar_event('User event', $user->id, 'due', 0, $now + 2*DAYSECS, $params + ['courseid' => 0]); // Retrieve course events for the second student - only one "Base event" is returned. $this->setUser($user2); $paramevents = array('courseids' => array($course->id)); $options = array ('siteevents' => true, 'userevents' => true); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(1, count($events['events'])); $this->assertEquals(0, count($events['warnings'])); $this->assertEquals('Base event', $events['events'][0]['name']); // Retrieve events for the first student - both events are returned. $this->setUser($user); $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(2, count($events['events'])); $this->assertEquals(0, count($events['warnings'])); $this->assertEquals('Base event', $events['events'][0]['name']); $this->assertEquals('User event', $events['events'][1]['name']); // Retrieve events by id as a teacher, 'User event' should be returned since teacher has access to this course. $this->setUser($teacher); $paramevents = ['eventids' => [$event2->id]]; $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(1, count($events['events'])); $this->assertEquals(0, count($events['warnings'])); $this->assertEquals('User event', $events['events'][0]['name']); // Retrieve events by id as another user, nothing should be returned. $this->setUser($anotheruser); $paramevents = ['eventids' => [$event2->id, $event1->id]]; $events = core_calendar_external::get_calendar_events($paramevents, $options); $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events); $this->assertEquals(0, count($events['events'])); $this->assertEquals(0, count($events['warnings'])); } /** * Requesting calendar events within a given time range should return all events with * a sort time between the lower and upper time bound (inclusive). * * If there are no events in the given time range then an empty result set should be * returned. */ public function test_get_calendar_action_events_by_timesort_time_range(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->resetAfterTest(true); $this->setUser($user); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'modulename' => 'assign', 'instance' => $moduleinstance->id, 'courseid' => $course->id, ]; $event1 = $this->create_calendar_event('Event 1', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 1])); $event2 = $this->create_calendar_event('Event 2', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 2])); $event3 = $this->create_calendar_event('Event 3', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 3])); $event4 = $this->create_calendar_event('Event 4', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 4])); $event5 = $this->create_calendar_event('Event 5', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 5])); $event6 = $this->create_calendar_event('Event 6', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 6])); $event7 = $this->create_calendar_event('Event 7', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 7])); $event8 = $this->create_calendar_event('Event 8', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 8])); $result = core_calendar_external::get_calendar_action_events_by_timesort(3, 6); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_timesort_returns(), $result ); $events = $result['events']; $this->assertCount(4, $events); $this->assertEquals('Event 3', $events[0]['name']); $this->assertEquals('Event 4', $events[1]['name']); $this->assertEquals('Event 5', $events[2]['name']); $this->assertEquals('Event 6', $events[3]['name']); $this->assertEquals($event3->id, $result['firstid']); $this->assertEquals($event6->id, $result['lastid']); $result = core_calendar_external::get_calendar_action_events_by_timesort(10, 15); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_timesort_returns(), $result ); $this->assertEmpty($result['events']); $this->assertNull($result['firstid']); $this->assertNull($result['lastid']); } /** * Requesting calendar events within a given time range and a limit and offset should return * the number of events up to the given limit value that have a sort time between the lower * and uppper time bound (inclusive) where the result set is shifted by the offset value. * * If there are no events in the given time range then an empty result set should be * returned. */ public function test_get_calendar_action_events_by_timesort_time_limit_offset(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->resetAfterTest(true); $this->setUser($user); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'modulename' => 'assign', 'instance' => $moduleinstance->id, 'courseid' => $course->id, ]; $event1 = $this->create_calendar_event('Event 1', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 1])); $event2 = $this->create_calendar_event('Event 2', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 2])); $event3 = $this->create_calendar_event('Event 3', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 3])); $event4 = $this->create_calendar_event('Event 4', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 4])); $event5 = $this->create_calendar_event('Event 5', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 5])); $event6 = $this->create_calendar_event('Event 6', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 6])); $event7 = $this->create_calendar_event('Event 7', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 7])); $event8 = $this->create_calendar_event('Event 8', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 8])); $result = core_calendar_external::get_calendar_action_events_by_timesort(2, 7, $event3->id, 2); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_timesort_returns(), $result ); $events = $result['events']; $this->assertCount(2, $events); $this->assertEquals('Event 4', $events[0]['name']); $this->assertEquals('Event 5', $events[1]['name']); $this->assertEquals($event4->id, $result['firstid']); $this->assertEquals($event5->id, $result['lastid']); $result = core_calendar_external::get_calendar_action_events_by_timesort(2, 7, $event5->id, 2); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_timesort_returns(), $result ); $events = $result['events']; $this->assertCount(2, $events); $this->assertEquals('Event 6', $events[0]['name']); $this->assertEquals('Event 7', $events[1]['name']); $this->assertEquals($event6->id, $result['firstid']); $this->assertEquals($event7->id, $result['lastid']); $result = core_calendar_external::get_calendar_action_events_by_timesort(2, 7, $event7->id, 2); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_timesort_returns(), $result ); $this->assertEmpty($result['events']); $this->assertNull($result['firstid']); $this->assertNull($result['lastid']); } /** * Check that it is possible to restrict the calendar events to events where the user is not suspended in the course. */ public function test_get_calendar_action_events_by_timesort_suspended_course(): void { $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $this->setAdminUser(); $lesson = $this->getDataGenerator()->create_module('lesson', [ 'name' => 'Lesson 1', 'course' => $course->id, 'available' => time(), 'deadline' => (time() + (60 * 60 * 24 * 5)) ] ); $this->getDataGenerator()->enrol_user($user1->id, $course->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED); $this->getDataGenerator()->enrol_user($user2->id, $course->id); $this->setUser($user1); $result = core_calendar_external::get_calendar_action_events_by_timesort(0, null, 0, 20, true); $this->assertEmpty($result->events); $this->setUser($user2); $result = core_calendar_external::get_calendar_action_events_by_timesort(0, null, 0, 20, true); $this->assertCount(1, $result->events); $this->assertEquals('Lesson 1 closes', $result->events[0]->name); } /** * Check that it is possible to get other user's events without the permission. */ public function test_get_calendar_action_events_by_timesort_for_other_users(): void { $this->resetAfterTest(); // Create test users. $user1 = $this->getDataGenerator()->create_user(['email' => 'student1@localhost.com']); $user2 = $this->getDataGenerator()->create_user(['email' => 'student2@localhost.com']); // Create test course. $course = $this->getDataGenerator()->create_course(); $this->setAdminUser(); // Create test activity and make it available only for student2. $lesson = $this->getDataGenerator()->create_module('lesson', [ 'name' => 'Lesson 1', 'course' => $course->id, 'available' => time(), 'deadline' => (time() + (60 * 60 * 24 * 5)), 'availability' => '{"op":"&","c":[{"type":"profile","sf":"email","op":"isequalto","v":"student2@localhost.com"}],"showc":[true]}' ] ); // Enrol. $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); // Student2 can see the event. $this->setUser($user2); $result = core_calendar_external::get_calendar_action_events_by_timesort(0, null, 0, 20, true); $this->assertCount(1, $result->events); $this->assertEquals('Lesson 1 closes', $result->events[0]->name); // Student1 cannot see the event. $this->setUser($user1); $result = core_calendar_external::get_calendar_action_events_by_timesort(0, null, 0, 20, true); $this->assertEmpty($result->events); // Admin, Manager, Teacher can view student2's data. $this->setAdminUser(); $result = core_calendar_external::get_calendar_action_events_by_timesort(0, null, 0, 20, true, $user2->id); $this->assertCount(1, $result->events); $this->assertEquals('Lesson 1 closes', $result->events[0]->name); // Student1 will see an exception if he/she trying to view student2's data. $this->setUser($user1); $this->expectException(\required_capability_exception::class); $this->expectExceptionMessage('error/nopermission'); $result = core_calendar_external::get_calendar_action_events_by_timesort(0, null, 0, 20, true, $user2->id); } /** * Requesting calendar events from a given course and time should return all * events with a sort time at or after the requested time. All events prior * to that time should not be return. * * If there are no events on or after the given time then an empty result set should * be returned. */ public function test_get_calendar_action_events_by_course_after_time(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $instance1 = $generator->create_instance(['course' => $course1->id]); $instance2 = $generator->create_instance(['course' => $course2->id]); $records = []; $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->resetAfterTest(true); $this->setUser($user); for ($i = 1; $i < 19; $i++) { $courseid = ($i < 9) ? $course1->id : $course2->id; $instance = ($i < 9) ? $instance1->id : $instance2->id; $records[] = $this->create_calendar_event( sprintf('Event %d', $i), $user->id, 'user', 0, 1, [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $courseid, 'timesort' => $i, 'modulename' => 'assign', 'instance' => $instance, ] ); } $result = core_calendar_external::get_calendar_action_events_by_course($course1->id, 5); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_course_returns(), $result ); $result = $result['events']; $this->assertCount(4, $result); $this->assertEquals('Event 5', $result[0]['name']); $this->assertEquals('Event 6', $result[1]['name']); $this->assertEquals('Event 7', $result[2]['name']); $this->assertEquals('Event 8', $result[3]['name']); $result = core_calendar_external::get_calendar_action_events_by_course($course1->id, 9); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_course_returns(), $result ); $result = $result['events']; $this->assertEmpty($result); } /** * Requesting calendar events for a course and before a given time should return * all events with a sort time at or before the requested time (inclusive). All * events after that time should not be returned. * * If there are no events before the given time then an empty result set should be * returned. */ public function test_get_calendar_action_events_by_course_before_time(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $instance1 = $generator->create_instance(['course' => $course1->id]); $instance2 = $generator->create_instance(['course' => $course2->id]); $records = []; $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->resetAfterTest(true); $this->setUser($user); for ($i = 1; $i < 19; $i++) { $courseid = ($i < 9) ? $course1->id : $course2->id; $instance = ($i < 9) ? $instance1->id : $instance2->id; $records[] = $this->create_calendar_event( sprintf('Event %d', $i), $user->id, 'user', 0, 1, [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $courseid, 'timesort' => $i + 1, 'modulename' => 'assign', 'instance' => $instance, ] ); } $result = core_calendar_external::get_calendar_action_events_by_course($course1->id, null, 5); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_course_returns(), $result ); $result = $result['events']; $this->assertCount(4, $result); $this->assertEquals('Event 1', $result[0]['name']); $this->assertEquals('Event 2', $result[1]['name']); $this->assertEquals('Event 3', $result[2]['name']); $this->assertEquals('Event 4', $result[3]['name']); $result = core_calendar_external::get_calendar_action_events_by_course($course1->id, null, 1); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_course_returns(), $result ); $result = $result['events']; $this->assertEmpty($result); } /** * Requesting calendar events for a course and within a given time range should * return all events with a sort time between the lower and upper time bound * (inclusive). * * If there are no events in the given time range then an empty result set should be * returned. */ public function test_get_calendar_action_events_by_course_time_range(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $instance1 = $generator->create_instance(['course' => $course1->id]); $instance2 = $generator->create_instance(['course' => $course2->id]); $records = []; $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->resetAfterTest(true); $this->setUser($user); for ($i = 1; $i < 19; $i++) { $courseid = ($i < 9) ? $course1->id : $course2->id; $instance = ($i < 9) ? $instance1->id : $instance2->id; $records[] = $this->create_calendar_event( sprintf('Event %d', $i), $user->id, 'user', 0, 1, [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $courseid, 'timesort' => $i, 'modulename' => 'assign', 'instance' => $instance, ] ); } $result = core_calendar_external::get_calendar_action_events_by_course($course1->id, 3, 6); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_course_returns(), $result ); $result = $result['events']; $this->assertCount(4, $result); $this->assertEquals('Event 3', $result[0]['name']); $this->assertEquals('Event 4', $result[1]['name']); $this->assertEquals('Event 5', $result[2]['name']); $this->assertEquals('Event 6', $result[3]['name']); $result = core_calendar_external::get_calendar_action_events_by_course($course1->id, 10, 15); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_course_returns(), $result ); $result = $result['events']; $this->assertEmpty($result); } /** * Requesting calendar events for a course and within a given time range and a limit * and offset should return the number of events up to the given limit value that have * a sort time between the lower and uppper time bound (inclusive) where the result * set is shifted by the offset value. * * If there are no events in the given time range then an empty result set should be * returned. */ public function test_get_calendar_action_events_by_course_time_limit_offset(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $instance1 = $generator->create_instance(['course' => $course1->id]); $instance2 = $generator->create_instance(['course' => $course2->id]); $records = []; $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->resetAfterTest(true); $this->setUser($user); for ($i = 1; $i < 19; $i++) { $courseid = ($i < 9) ? $course1->id : $course2->id; $instance = ($i < 9) ? $instance1->id : $instance2->id; $records[] = $this->create_calendar_event( sprintf('Event %d', $i), $user->id, 'user', 0, 1, [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $courseid, 'timesort' => $i, 'modulename' => 'assign', 'instance' => $instance, ] ); } $result = core_calendar_external::get_calendar_action_events_by_course( $course1->id, 2, 7, $records[2]->id, 2); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_course_returns(), $result ); $result = $result['events']; $this->assertCount(2, $result); $this->assertEquals('Event 4', $result[0]['name']); $this->assertEquals('Event 5', $result[1]['name']); $result = core_calendar_external::get_calendar_action_events_by_course( $course1->id, 2, 7, $records[4]->id, 2); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_course_returns(), $result ); $result = $result['events']; $this->assertCount(2, $result); $this->assertEquals('Event 6', $result[0]['name']); $this->assertEquals('Event 7', $result[1]['name']); $result = core_calendar_external::get_calendar_action_events_by_course( $course1->id, 2, 7, $records[6]->id, 2); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_course_returns(), $result ); $result = $result['events']; $this->assertEmpty($result); } /** * Test get_calendar_action_events_by_course with search feature */ public function test_get_calendar_action_events_by_course_with_search(): void { // Generate data. $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $instance = $generator->create_instance(['course' => $course->id]); // Enrol. $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->resetAfterTest(true); $this->setUser($user); for ($i = 1; $i < 5; $i++) { $this->create_calendar_event( sprintf('Event %d', $i), $user->id, 'user', 0, 1, [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course->id, 'timesort' => $i, 'modulename' => 'assign', 'instance' => $instance->id, ] ); } // No result found for fake search. $result = core_calendar_external::get_calendar_action_events_by_course($course->id, null, null, 0, 20, 'Fake search'); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_course_returns(), $result ); $result = $result['events']; $this->assertEmpty($result); // Search for event name called 'Event 1'. $result = core_calendar_external::get_calendar_action_events_by_course($course->id, null, null, 0, 20, 'Event 1'); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_course_returns(), $result ); $result = $result['events']; $this->assertCount(1, $result); $this->assertEquals('Event 1', $result[0]['name']); // Search for activity type called 'assign'. $result = core_calendar_external::get_calendar_action_events_by_course($course->id, null, null, 0, 20, 'assign'); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_course_returns(), $result ); $result = $result['events']; $this->assertCount(4, $result); $this->assertEquals('Event 1', $result[0]['name']); $this->assertEquals('Event 2', $result[1]['name']); $this->assertEquals('Event 3', $result[2]['name']); $this->assertEquals('Event 4', $result[3]['name']); } /** * Test that get_action_events_by_courses will return a list of events for each * course you provided as long as the user is enrolled in the course. */ public function test_get_action_events_by_courses(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $instance1 = $generator->create_instance(['course' => $course1->id]); $instance2 = $generator->create_instance(['course' => $course2->id]); $instance3 = $generator->create_instance(['course' => $course3->id]); $records = []; $mapresult = function($result) { $groupedbycourse = []; foreach ($result['groupedbycourse'] as $group) { $events = $group['events']; $courseid = $group['courseid']; $groupedbycourse[$courseid] = $events; } return $groupedbycourse; }; $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->resetAfterTest(true); $this->setUser($user); for ($i = 1; $i < 10; $i++) { if ($i < 3) { $courseid = $course1->id; $instance = $instance1->id; } else if ($i < 6) { $courseid = $course2->id; $instance = $instance2->id; } else { $courseid = $course3->id; $instance = $instance3->id; } $records[] = $this->create_calendar_event( sprintf('Event %d', $i), $user->id, 'user', 0, 1, [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $courseid, 'timesort' => $i, 'modulename' => 'assign', 'instance' => $instance, ] ); } $result = core_calendar_external::get_calendar_action_events_by_courses([], 1); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_courses_returns(), $result ); $result = $result['groupedbycourse']; $this->assertEmpty($result); $result = core_calendar_external::get_calendar_action_events_by_courses([$course1->id], 3); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_courses_returns(), $result ); $groupedbycourse = $mapresult($result); $this->assertEmpty($groupedbycourse[$course1->id]); $result = core_calendar_external::get_calendar_action_events_by_courses([$course1->id], 1); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_courses_returns(), $result ); $groupedbycourse = $mapresult($result); $this->assertCount(2, $groupedbycourse[$course1->id]); $this->assertEquals('Event 1', $groupedbycourse[$course1->id][0]['name']); $this->assertEquals('Event 2', $groupedbycourse[$course1->id][1]['name']); $result = core_calendar_external::get_calendar_action_events_by_courses( [$course1->id, $course2->id], 1); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_courses_returns(), $result ); $groupedbycourse = $mapresult($result); $this->assertCount(2, $groupedbycourse[$course1->id]); $this->assertEquals('Event 1', $groupedbycourse[$course1->id][0]['name']); $this->assertEquals('Event 2', $groupedbycourse[$course1->id][1]['name']); $this->assertCount(3, $groupedbycourse[$course2->id]); $this->assertEquals('Event 3', $groupedbycourse[$course2->id][0]['name']); $this->assertEquals('Event 4', $groupedbycourse[$course2->id][1]['name']); $this->assertEquals('Event 5', $groupedbycourse[$course2->id][2]['name']); $result = core_calendar_external::get_calendar_action_events_by_courses( [$course1->id, $course2->id], 2, 4); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_courses_returns(), $result ); $groupedbycourse = $mapresult($result); $this->assertCount(2, $groupedbycourse); $this->assertCount(1, $groupedbycourse[$course1->id]); $this->assertEquals('Event 2', $groupedbycourse[$course1->id][0]['name']); $this->assertCount(2, $groupedbycourse[$course2->id]); $this->assertEquals('Event 3', $groupedbycourse[$course2->id][0]['name']); $this->assertEquals('Event 4', $groupedbycourse[$course2->id][1]['name']); $result = core_calendar_external::get_calendar_action_events_by_courses( [$course1->id, $course2->id], 1, null, 1); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_courses_returns(), $result ); $groupedbycourse = $mapresult($result); $this->assertCount(2, $groupedbycourse); $this->assertCount(1, $groupedbycourse[$course1->id]); $this->assertEquals('Event 1', $groupedbycourse[$course1->id][0]['name']); $this->assertCount(1, $groupedbycourse[$course2->id]); $this->assertEquals('Event 3', $groupedbycourse[$course2->id][0]['name']); } /** * Test get_action_events_by_courses with search feature */ public function test_get_action_events_by_courses_with_search(): void { // Generate data. $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $instance1 = $generator->create_instance(['course' => $course1->id]); $instance2 = $generator->create_instance(['course' => $course2->id]); $instance3 = $generator->create_instance(['course' => $course3->id]); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->resetAfterTest(true); $this->setUser($user); $mapresult = function($result) { $groupedbycourse = []; foreach ($result['groupedbycourse'] as $group) { $events = $group['events']; $courseid = $group['courseid']; $groupedbycourse[$courseid] = $events; } return $groupedbycourse; }; for ($i = 1; $i < 10; $i++) { if ($i < 3) { $courseid = $course1->id; $instance = $instance1->id; } else if ($i < 6) { $courseid = $course2->id; $instance = $instance2->id; } else { $courseid = $course3->id; $instance = $instance3->id; } $records[] = $this->create_calendar_event( sprintf('Event %d', $i), $user->id, 'user', 0, 1, [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $courseid, 'timesort' => $i, 'modulename' => 'assign', 'instance' => $instance, ] ); } // No result found for fake search. $result = core_calendar_external::get_calendar_action_events_by_courses([$course1->id, $course2->id, $course3->id], 1, null, 20, 'Fake search'); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_courses_returns(), $result ); $groupedbycourse = $mapresult($result); $this->assertEmpty($groupedbycourse[$course1->id]); $this->assertEmpty($groupedbycourse[$course2->id]); $this->assertArrayNotHasKey($course3->id, $groupedbycourse); // Search for event name called 'Event 1'. $result = core_calendar_external::get_calendar_action_events_by_courses([$course1->id, $course2->id, $course3->id], 1, null, 20, 'Event 1'); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_courses_returns(), $result ); $groupedbycourse = $mapresult($result); $this->assertArrayNotHasKey($course3->id, $groupedbycourse); $this->assertCount(2, $groupedbycourse); $this->assertCount(1, $groupedbycourse[$course1->id]); $this->assertCount(0, $groupedbycourse[$course2->id]); $this->assertEquals('Event 1', $groupedbycourse[$course1->id][0]['name']); // Search for activity type called 'assign'. $result = core_calendar_external::get_calendar_action_events_by_courses([$course1->id, $course2->id, $course3->id], 1, null, 20, 'assign'); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_courses_returns(), $result ); $groupedbycourse = $mapresult($result); $this->assertArrayNotHasKey($course3->id, $groupedbycourse); $this->assertCount(2, $groupedbycourse); $this->assertCount(2, $groupedbycourse[$course1->id]); $this->assertCount(3, $groupedbycourse[$course2->id]); $this->assertEquals('Event 1', $groupedbycourse[$course1->id][0]['name']); $this->assertEquals('Event 2', $groupedbycourse[$course1->id][1]['name']); $this->assertEquals('Event 3', $groupedbycourse[$course2->id][0]['name']); $this->assertEquals('Event 4', $groupedbycourse[$course2->id][1]['name']); $this->assertEquals('Event 5', $groupedbycourse[$course2->id][2]['name']); } /** * Test for deleting module events. */ public function test_delete_calendar_events_for_modules(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $nexttime = time() + DAYSECS; $this->getDataGenerator()->create_module('assign', ['course' => $course->id, 'duedate' => $nexttime]); $events = calendar_get_events(time(), $nexttime, true, true, true); $this->assertCount(1, $events); $params = []; foreach ($events as $event) { $params[] = [ 'eventid' => $event->id, 'repeat' => false ]; } $this->expectException(\moodle_exception::class); core_calendar_external::delete_calendar_events($params); } /** * Updating the event start day should change the date value but leave * the time of day unchanged. */ public function test_update_event_start_day(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $roleid = $generator->create_role(); $context = \context_system::instance(); $originalstarttime = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2018-02-2T10:00:00+08:00'); $expected = new \DateTimeImmutable('2018-02-2T15:00:00+08:00'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageownentries', CAP_ALLOW, $roleid, $context, true); $this->setUser($user); $this->resetAfterTest(true); $event = $this->create_calendar_event( 'Test event', $user->id, 'user', 0, null, [ 'courseid' => 0, 'timestart' => $originalstarttime->getTimestamp() ] ); $result = core_calendar_external::update_event_start_day($event->id, $newstartdate->getTimestamp()); $result = external_api::clean_returnvalue( core_calendar_external::update_event_start_day_returns(), $result ); $this->assertEquals($expected->getTimestamp(), $result['event']['timestart']); } /** * A user should not be able to edit an event that they don't have * capabilities for. */ public function test_update_event_start_day_no_permission(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $roleid = $generator->create_role(); $context = \context_system::instance(); $originalstarttime = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2018-02-2T10:00:00+08:00'); $expected = new \DateTimeImmutable('2018-02-2T15:00:00+08:00'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageownentries', CAP_ALLOW, $roleid, $context, true); $this->setUser($user); $this->resetAfterTest(true); $event = $this->create_calendar_event( 'Test event', $user->id, 'user', 0, null, [ 'courseid' => 0, 'timestart' => $originalstarttime->getTimestamp() ] ); assign_capability('moodle/calendar:manageownentries', CAP_PROHIBIT, $roleid, $context, true); $this->expectException(\moodle_exception::class); $result = core_calendar_external::update_event_start_day($event->id, $newstartdate->getTimestamp()); $result = external_api::clean_returnvalue( core_calendar_external::update_event_start_day_returns(), $result ); } /** * A user should not be able to update a module event. */ public function test_update_event_start_day_module_event(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $plugingenerator = $generator->get_plugin_generator('mod_assign'); $moduleinstance = $plugingenerator->create_instance(['course' => $course->id]); $roleid = $generator->create_role(); $context = \context_course::instance($course->id); $originalstarttime = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2018-02-2T10:00:00+08:00'); $expected = new \DateTimeImmutable('2018-02-2T15:00:00+08:00'); $generator->role_assign($roleid, $user->id, $context->id); $generator->enrol_user($user->id, $course->id); $this->setUser($user); $this->resetAfterTest(true); $event = $this->create_calendar_event( 'Test event', $user->id, 'user', 0, null, [ 'modulename' => 'assign', 'instance' => $moduleinstance->id, 'courseid' => $course->id, 'timestart' => $originalstarttime->getTimestamp() ] ); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); $this->expectException(\moodle_exception::class); $result = core_calendar_external::update_event_start_day($event->id, $newstartdate->getTimestamp()); $result = external_api::clean_returnvalue( core_calendar_external::update_event_start_day_returns(), $result ); } /** * Submit a request where the time duration until is earlier than the time * start in order to get a validation error from the server. */ public function test_submit_create_update_form_validation_error(): void { $user = $this->getDataGenerator()->create_user(); $timestart = new \DateTime(); $interval = new \DateInterval("P1D"); // One day. $timedurationuntil = new \DateTime(); $timedurationuntil->sub($interval); $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'name' => 'Test', 'timestart' => [ 'day' => $timestart->format('j'), 'month' => $timestart->format('n'), 'year' => $timestart->format('Y'), 'hour' => $timestart->format('G'), 'minute' => 0, ], 'eventtype' => 'user', 'description' => [ 'text' => '', 'format' => 1, ], 'location' => 'Test', 'duration' => 1, 'timedurationuntil' => [ 'day' => $timedurationuntil->format('j'), 'month' => $timedurationuntil->format('n'), 'year' => $timedurationuntil->format('Y'), 'hour' => $timedurationuntil->format('G'), 'minute' => 0, ] ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); $this->resetAfterTest(true); $this->setUser($user); $result = external_api::clean_returnvalue( core_calendar_external::submit_create_update_form_returns(), core_calendar_external::submit_create_update_form($querystring) ); $this->assertTrue($result['validationerror']); } /** * A user with the moodle/calendar:manageownentries capability at the * system context should be able to create a user event. */ public function test_submit_create_update_form_create_user_event(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $roleid = $generator->create_role(); $context = \context_system::instance(); $timestart = new \DateTime(); $interval = new \DateInterval("P1D"); // One day. $timedurationuntil = new \DateTime(); $timedurationuntil->add($interval); $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'name' => 'Test', 'timestart' => [ 'day' => $timestart->format('j'), 'month' => $timestart->format('n'), 'year' => $timestart->format('Y'), 'hour' => $timestart->format('G'), 'minute' => 0, ], 'eventtype' => 'user', 'description' => [ 'text' => '', 'format' => 1, 'itemid' => 0 ], 'location' => 'Test', 'duration' => 1, 'timedurationuntil' => [ 'day' => $timedurationuntil->format('j'), 'month' => $timedurationuntil->format('n'), 'year' => $timedurationuntil->format('Y'), 'hour' => $timedurationuntil->format('G'), 'minute' => 0, ] ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageownentries', CAP_ALLOW, $roleid, $context, true); $user->ignoresesskey = true; $this->resetAfterTest(true); $this->setUser($user); $result = external_api::clean_returnvalue( core_calendar_external::submit_create_update_form_returns(), core_calendar_external::submit_create_update_form($querystring) ); $event = $result['event']; $this->assertEquals($user->id, $event['userid']); $this->assertEquals($formdata['eventtype'], $event['eventtype']); $this->assertEquals($formdata['name'], $event['name']); } /** * A user without the moodle/calendar:manageownentries capability at the * system context should not be able to create a user event. */ public function test_submit_create_update_form_create_user_event_no_permission(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $roleid = $generator->create_role(); $context = \context_system::instance(); $timestart = new \DateTime(); $interval = new \DateInterval("P1D"); // One day. $timedurationuntil = new \DateTime(); $timedurationuntil->add($interval); $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'name' => 'Test', 'timestart' => [ 'day' => $timestart->format('j'), 'month' => $timestart->format('n'), 'year' => $timestart->format('Y'), 'hour' => $timestart->format('G'), 'minute' => 0, ], 'eventtype' => 'user', 'description' => [ 'text' => '', 'format' => 1, ], 'location' => 'Test', 'duration' => 1, 'timedurationuntil' => [ 'day' => $timedurationuntil->format('j'), 'month' => $timedurationuntil->format('n'), 'year' => $timedurationuntil->format('Y'), 'hour' => $timedurationuntil->format('G'), 'minute' => 0, ] ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageownentries', CAP_PROHIBIT, $roleid, $context, true); $user->ignoresesskey = true; $this->resetAfterTest(true); $this->setUser($user); $this->expectException(\moodle_exception::class); external_api::clean_returnvalue( core_calendar_external::submit_create_update_form_returns(), core_calendar_external::submit_create_update_form($querystring) ); } /** * A user with the moodle/calendar:manageentries capability at the * site course context should be able to create a site event. */ public function test_submit_create_update_form_create_site_event(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $context = \context_system::instance(); $roleid = $generator->create_role(); $timestart = new \DateTime(); $interval = new \DateInterval("P1D"); // One day. $timedurationuntil = new \DateTime(); $timedurationuntil->add($interval); $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'name' => 'Test', 'timestart' => [ 'day' => $timestart->format('j'), 'month' => $timestart->format('n'), 'year' => $timestart->format('Y'), 'hour' => $timestart->format('G'), 'minute' => 0, ], 'eventtype' => 'site', 'description' => [ 'text' => '', 'format' => 1, 'itemid' => 0 ], 'location' => 'Test', 'duration' => 1, 'timedurationuntil' => [ 'day' => $timedurationuntil->format('j'), 'month' => $timedurationuntil->format('n'), 'year' => $timedurationuntil->format('Y'), 'hour' => $timedurationuntil->format('G'), 'minute' => 0, ] ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); $user->ignoresesskey = true; $this->resetAfterTest(true); $this->setUser($user); $result = external_api::clean_returnvalue( core_calendar_external::submit_create_update_form_returns(), core_calendar_external::submit_create_update_form($querystring) ); $event = $result['event']; $this->assertEquals($user->id, $event['userid']); $this->assertEquals($formdata['eventtype'], $event['eventtype']); $this->assertEquals($formdata['name'], $event['name']); } /** * A user without the moodle/calendar:manageentries capability at the * site course context should not be able to create a site event. */ public function test_submit_create_update_form_create_site_event_no_permission(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $context = \context_course::instance(SITEID); $roleid = $generator->create_role(); $timestart = new \DateTime(); $interval = new \DateInterval("P1D"); // One day. $timedurationuntil = new \DateTime(); $timedurationuntil->add($interval); $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'name' => 'Test', 'timestart' => [ 'day' => $timestart->format('j'), 'month' => $timestart->format('n'), 'year' => $timestart->format('Y'), 'hour' => $timestart->format('G'), 'minute' => 0, ], 'eventtype' => 'site', 'description' => [ 'text' => '', 'format' => 1, ], 'location' => 'Test', 'duration' => 1, 'timedurationuntil' => [ 'day' => $timedurationuntil->format('j'), 'month' => $timedurationuntil->format('n'), 'year' => $timedurationuntil->format('Y'), 'hour' => $timedurationuntil->format('G'), 'minute' => 0, ] ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true); $user->ignoresesskey = true; $this->resetAfterTest(true); $this->setUser($user); $result = external_api::clean_returnvalue( core_calendar_external::submit_create_update_form_returns(), core_calendar_external::submit_create_update_form($querystring) ); $this->assertTrue($result['validationerror']); } /** * A user that has the moodle/calendar:manageentries in a course that they * are enrolled in should be able to create a course event in that course. */ public function test_submit_create_update_form_create_course_event(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $timestart = new \DateTime(); $interval = new \DateInterval("P1D"); // One day. $timedurationuntil = new \DateTime(); $timedurationuntil->add($interval); $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'name' => 'Test', 'timestart' => [ 'day' => $timestart->format('j'), 'month' => $timestart->format('n'), 'year' => $timestart->format('Y'), 'hour' => $timestart->format('G'), 'minute' => 0, ], 'eventtype' => 'course', 'courseid' => $course->id, 'description' => [ 'text' => '', 'format' => 1, 'itemid' => 0, ], 'location' => 'Test', 'duration' => 1, 'timedurationuntil' => [ 'day' => $timedurationuntil->format('j'), 'month' => $timedurationuntil->format('n'), 'year' => $timedurationuntil->format('Y'), 'hour' => $timedurationuntil->format('G'), 'minute' => 0, ] ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); $user->ignoresesskey = true; $this->resetAfterTest(true); $this->setUser($user); $result = external_api::clean_returnvalue( core_calendar_external::submit_create_update_form_returns(), core_calendar_external::submit_create_update_form($querystring) ); $event = $result['event']; $this->assertEquals($user->id, $event['userid']); $this->assertEquals($formdata['eventtype'], $event['eventtype']); $this->assertEquals($formdata['name'], $event['name']); $this->assertEquals($formdata['courseid'], $event['course']['id']); } /** * A user without the moodle/calendar:manageentries capability in a course * that they are enrolled in should not be able to create a course event in that course. */ public function test_submit_create_update_form_create_course_event_no_permission(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $timestart = new \DateTime(); $interval = new \DateInterval("P1D"); // One day. $timedurationuntil = new \DateTime(); $timedurationuntil->add($interval); $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'name' => 'Test', 'timestart' => [ 'day' => $timestart->format('j'), 'month' => $timestart->format('n'), 'year' => $timestart->format('Y'), 'hour' => $timestart->format('G'), 'minute' => 0, ], 'eventtype' => 'course', 'courseid' => $course->id, 'description' => [ 'text' => '', 'format' => 1, ], 'location' => 'Test', 'duration' => 1, 'timedurationuntil' => [ 'day' => $timedurationuntil->format('j'), 'month' => $timedurationuntil->format('n'), 'year' => $timedurationuntil->format('Y'), 'hour' => $timedurationuntil->format('G'), 'minute' => 0, ] ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true); $user->ignoresesskey = true; $this->resetAfterTest(true); $this->setUser($user); $result = external_api::clean_returnvalue( core_calendar_external::submit_create_update_form_returns(), core_calendar_external::submit_create_update_form($querystring) ); $this->assertTrue($result['validationerror']); } /** * A user should not be able to create an event for a course that they are * not enrolled in. */ public function test_submit_create_update_form_create_course_event_not_enrolled(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $course2 = $generator->create_course(); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $timestart = new \DateTime(); $interval = new \DateInterval("P1D"); // One day. $timedurationuntil = new \DateTime(); $timedurationuntil->add($interval); $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'name' => 'Test', 'timestart' => [ 'day' => $timestart->format('j'), 'month' => $timestart->format('n'), 'year' => $timestart->format('Y'), 'hour' => $timestart->format('G'), 'minute' => 0, ], 'eventtype' => 'course', 'courseid' => $course2->id, // Not enrolled. 'description' => [ 'text' => '', 'format' => 1, ], 'location' => 'Test', 'duration' => 1, 'timedurationuntil' => [ 'day' => $timedurationuntil->format('j'), 'month' => $timedurationuntil->format('n'), 'year' => $timedurationuntil->format('Y'), 'hour' => $timedurationuntil->format('G'), 'minute' => 0, ] ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); $user->ignoresesskey = true; $this->resetAfterTest(true); $this->setUser($user); $result = external_api::clean_returnvalue( core_calendar_external::submit_create_update_form_returns(), core_calendar_external::submit_create_update_form($querystring) ); $this->assertTrue($result['validationerror']); } /** * A user should be able to create an event for a group that they are a member of in * a course in which they are enrolled and have the moodle/calendar:manageentries capability. */ public function test_submit_create_update_form_create_group_event_group_member_manage_course(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $group = $generator->create_group(array('courseid' => $course->id)); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $timestart = new \DateTime(); $interval = new \DateInterval("P1D"); // One day. $timedurationuntil = new \DateTime(); $timedurationuntil->add($interval); $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'name' => 'Test', 'timestart' => [ 'day' => $timestart->format('j'), 'month' => $timestart->format('n'), 'year' => $timestart->format('Y'), 'hour' => $timestart->format('G'), 'minute' => 0, ], 'eventtype' => 'group', 'groupid' => $group->id, 'groupcourseid' => $course->id, 'description' => [ 'text' => '', 'format' => 1, 'itemid' => 0 ], 'location' => 'Test', 'duration' => 1, 'timedurationuntil' => [ 'day' => $timedurationuntil->format('j'), 'month' => $timedurationuntil->format('n'), 'year' => $timedurationuntil->format('Y'), 'hour' => $timedurationuntil->format('G'), 'minute' => 0, ] ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); $generator->create_group_member(['groupid' => $group->id, 'userid' => $user->id]); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); $user->ignoresesskey = true; $this->resetAfterTest(true); $this->setUser($user); $result = external_api::clean_returnvalue( core_calendar_external::submit_create_update_form_returns(), core_calendar_external::submit_create_update_form($querystring) ); $event = $result['event']; $this->assertEquals($user->id, $event['userid']); $this->assertEquals($formdata['eventtype'], $event['eventtype']); $this->assertEquals($formdata['name'], $event['name']); $this->assertEquals($group->id, $event['groupid']); } /** * A user should be able to create an event for a group that they are a member of in * a course in which they are enrolled and have the moodle/calendar:managegroupentries capability. */ public function test_submit_create_update_form_create_group_event_group_member_manage_group_entries(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $group = $generator->create_group(array('courseid' => $course->id)); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $timestart = new \DateTime(); $interval = new \DateInterval("P1D"); // One day. $timedurationuntil = new \DateTime(); $timedurationuntil->add($interval); $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'name' => 'Test', 'timestart' => [ 'day' => $timestart->format('j'), 'month' => $timestart->format('n'), 'year' => $timestart->format('Y'), 'hour' => $timestart->format('G'), 'minute' => 0, ], 'eventtype' => 'group', 'groupid' => $group->id, 'groupcourseid' => $course->id, 'description' => [ 'text' => '', 'format' => 1, 'itemid' => 0 ], 'location' => 'Test', 'duration' => 1, 'timedurationuntil' => [ 'day' => $timedurationuntil->format('j'), 'month' => $timedurationuntil->format('n'), 'year' => $timedurationuntil->format('Y'), 'hour' => $timedurationuntil->format('G'), 'minute' => 0, ] ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); $generator->create_group_member(['groupid' => $group->id, 'userid' => $user->id]); assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true); assign_capability('moodle/calendar:managegroupentries', CAP_ALLOW, $roleid, $context, true); $user->ignoresesskey = true; $this->resetAfterTest(true); $this->setUser($user); $result = external_api::clean_returnvalue( core_calendar_external::submit_create_update_form_returns(), core_calendar_external::submit_create_update_form($querystring) ); $event = $result['event']; $this->assertEquals($user->id, $event['userid']); $this->assertEquals($formdata['eventtype'], $event['eventtype']); $this->assertEquals($formdata['name'], $event['name']); $this->assertEquals($group->id, $event['groupid']); } /** * A user should be able to create an event for any group in a course in which * they are enrolled and have the moodle/site:accessallgroups capability. */ public function test_submit_create_update_form_create_group_event_access_all_groups(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $group = $generator->create_group(array('courseid' => $course->id)); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $timestart = new \DateTime(); $interval = new \DateInterval("P1D"); // One day. $timedurationuntil = new \DateTime(); $timedurationuntil->add($interval); $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'name' => 'Test', 'timestart' => [ 'day' => $timestart->format('j'), 'month' => $timestart->format('n'), 'year' => $timestart->format('Y'), 'hour' => $timestart->format('G'), 'minute' => 0, ], 'eventtype' => 'group', 'groupid' => $group->id, 'groupcourseid' => $course->id, 'description' => [ 'text' => '', 'format' => 1, 'itemid' => 0 ], 'location' => 'Test', 'duration' => 1, 'timedurationuntil' => [ 'day' => $timedurationuntil->format('j'), 'month' => $timedurationuntil->format('n'), 'year' => $timedurationuntil->format('Y'), 'hour' => $timedurationuntil->format('G'), 'minute' => 0, ] ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context, true); $user->ignoresesskey = true; $this->resetAfterTest(true); $this->setUser($user); $result = external_api::clean_returnvalue( core_calendar_external::submit_create_update_form_returns(), core_calendar_external::submit_create_update_form($querystring) ); $event = $result['event']; $this->assertEquals($user->id, $event['userid']); $this->assertEquals($formdata['eventtype'], $event['eventtype']); $this->assertEquals($formdata['name'], $event['name']); $this->assertEquals($group->id, $event['groupid']); } /** * A user should not be able to create an event for any group that they are not a * member of in a course in which they are enrolled but don't have the * moodle/site:accessallgroups capability. */ public function test_submit_create_update_form_create_group_event_non_member_no_permission(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $group = $generator->create_group(array('courseid' => $course->id)); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $timestart = new \DateTime(); $interval = new \DateInterval("P1D"); // One day. $timedurationuntil = new \DateTime(); $timedurationuntil->add($interval); $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'name' => 'Test', 'timestart' => [ 'day' => $timestart->format('j'), 'month' => $timestart->format('n'), 'year' => $timestart->format('Y'), 'hour' => $timestart->format('G'), 'minute' => 0, ], 'eventtype' => 'group', 'groupid' => $group->id, 'groupcourseid' => $course->id, 'description' => [ 'text' => '', 'format' => 1, ], 'location' => 'Test', 'duration' => 1, 'timedurationuntil' => [ 'day' => $timedurationuntil->format('j'), 'month' => $timedurationuntil->format('n'), 'year' => $timedurationuntil->format('Y'), 'hour' => $timedurationuntil->format('G'), 'minute' => 0, ] ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true); $user->ignoresesskey = true; $this->resetAfterTest(true); $this->setUser($user); $result = external_api::clean_returnvalue( core_calendar_external::submit_create_update_form_returns(), core_calendar_external::submit_create_update_form($querystring) ); $this->assertTrue($result['validationerror']); } /** * A user should not be able load the calendar monthly view for a course they cannot access. */ public function test_get_calendar_monthly_view_no_course_permission(): void { global $USER; $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user1 = $generator->create_user(); $user2 = $generator->create_user(); $course = $generator->create_course(); $generator->enrol_user($user1->id, $course->id, 'student'); $name = 'Course Event (course' . $course->id . ')'; $record = new \stdClass(); $record->courseid = $course->id; $courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record); $timestart = new \DateTime(); // Admin can load the course. $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_monthly_view_returns(), core_calendar_external::get_calendar_monthly_view($timestart->format('Y'), $timestart->format('n'), $course->id, null, false, true, $timestart->format('j')) ); $this->assertEquals($data['courseid'], $course->id); // User enrolled in the course can load the course calendar. $this->setUser($user1); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_monthly_view_returns(), core_calendar_external::get_calendar_monthly_view($timestart->format('Y'), $timestart->format('n'), $course->id, null, false, true, $timestart->format('j')) ); $this->assertEquals($data['courseid'], $course->id); // User not enrolled in the course cannot load the course calendar. $this->setUser($user2); $this->expectException(\require_login_exception::class); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_monthly_view_returns(), core_calendar_external::get_calendar_monthly_view($timestart->format('Y'), $timestart->format('n'), $course->id, null, false, false, $timestart->format('j')) ); } /** * Test get_calendar_monthly_view when a day parameter is provided. */ public function test_get_calendar_monthly_view_with_day_provided(): void { $this->resetAfterTest(); $this->setAdminUser(); $timestart = new \DateTime(); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_monthly_view_returns(), core_calendar_external::get_calendar_monthly_view($timestart->format('Y'), $timestart->format('n'), SITEID, null, false, true, $timestart->format('j')) ); $this->assertEquals($data['date']['mday'], $timestart->format('d')); } /** * A user should not be able load the calendar day view for a course they cannot access. */ public function test_get_calendar_day_view_no_course_permission(): void { global $USER; $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user1 = $generator->create_user(); $user2 = $generator->create_user(); $course = $generator->create_course(); $generator->enrol_user($user1->id, $course->id, 'student'); $name = 'Course Event (course' . $course->id . ')'; $record = new \stdClass(); $record->courseid = $course->id; $courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record); $timestart = new \DateTime(); // Admin can load the course. $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_day_view_returns(), core_calendar_external::get_calendar_day_view($timestart->format('Y'), $timestart->format('n'), $timestart->format('j'), $course->id, null) ); $this->assertEquals($data['courseid'], $course->id); // User enrolled in the course can load the course calendar. $this->setUser($user1); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_day_view_returns(), core_calendar_external::get_calendar_day_view($timestart->format('Y'), $timestart->format('n'), $timestart->format('j'), $course->id, null) ); $this->assertEquals($data['courseid'], $course->id); // User not enrolled in the course cannot load the course calendar. $this->setUser($user2); $this->expectException(\require_login_exception::class); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_day_view_returns(), core_calendar_external::get_calendar_day_view($timestart->format('Y'), $timestart->format('n'), $timestart->format('j'), $course->id, null) ); } /** * A user should not be able load the calendar upcoming view for a course they cannot access. */ public function test_get_calendar_upcoming_view_no_course_permission(): void { global $USER; $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user1 = $generator->create_user(); $user2 = $generator->create_user(); $course = $generator->create_course(); $generator->enrol_user($user1->id, $course->id, 'student'); $name = 'Course Event (course' . $course->id . ')'; $record = new \stdClass(); $record->courseid = $course->id; $courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record); // Admin can load the course. $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_upcoming_view_returns(), core_calendar_external::get_calendar_upcoming_view($course->id, null) ); $this->assertEquals($data['courseid'], $course->id); // User enrolled in the course can load the course calendar. $this->setUser($user1); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_upcoming_view_returns(), core_calendar_external::get_calendar_upcoming_view($course->id, null) ); $this->assertEquals($data['courseid'], $course->id); // User not enrolled in the course cannot load the course calendar. $this->setUser($user2); $this->expectException(\require_login_exception::class); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_upcoming_view_returns(), core_calendar_external::get_calendar_upcoming_view($course->id, null) ); } /** * A user should not be able load the calendar event for a course they cannot access. */ public function test_get_calendar_event_by_id_no_course_permission(): void { global $USER; $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user1 = $generator->create_user(); $user2 = $generator->create_user(); $course = $generator->create_course(); $generator->enrol_user($user1->id, $course->id, 'student'); $name = 'Course Event (course' . $course->id . ')'; $record = new \stdClass(); $record->courseid = $course->id; $courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record); // Admin can load the course event. $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_event_by_id_returns(), core_calendar_external::get_calendar_event_by_id($courseevent->id) ); $this->assertEquals($data['event']['id'], $courseevent->id); // User enrolled in the course can load the course event. $this->setUser($user1); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_event_by_id_returns(), core_calendar_external::get_calendar_event_by_id($courseevent->id) ); $this->assertEquals($data['event']['id'], $courseevent->id); // User not enrolled in the course cannot load the course event. $this->setUser($user2); $this->expectException(\moodle_exception::class); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_event_by_id_returns(), core_calendar_external::get_calendar_event_by_id($courseevent->id) ); } /** * User data for testing reading calendar events. * * @return array */ public static function get_calendar_event_by_id_prevent_read_other_users_events_data_provider(): array { $syscontext = \context_system::instance(); $managerrole = 'manager'; return [ [true, false, $syscontext, $managerrole, true], [false, false, $syscontext, $managerrole, false], [false, false, null, null, true], [false, true, null, null, false], ]; } /** * Prevent user from reading other user's event. * * @covers \core_calendar_external::get_calendar_event_by_id * @dataProvider get_calendar_event_by_id_prevent_read_other_users_events_data_provider * * @param bool $isadminevent Is admin's event * @param bool $isadmin Is current user admin user * @param null|stdClass $readerrolecontext Reader role context * @param null|string $readerrolename Role name * @param bool $expectexception Should the test throw exception */ public function test_get_calendar_event_by_id_prevent_read_other_users_events( bool $isadminevent, bool $isadmin, ?\stdClass $readerrolecontext, ?string $readerrolename, bool $expectexception): void { global $USER, $DB; $this->resetAfterTest(); $generator = $this->getDataGenerator(); if ($isadminevent) { $this->setAdminUser(); } else { $user = $generator->create_user(); $this->setUser($user); } $userevent = $this->create_calendar_event('user event', $USER->id, 'user', 0, time()); $results = external_api::clean_returnvalue( core_calendar_external::get_calendar_event_by_id_returns(), core_calendar_external::get_calendar_event_by_id($userevent->id) ); $event = reset($results); $this->assertEquals($userevent->id, $event['id']); if ($isadmin) { $this->setAdminUser(); } else { $reader = $generator->create_user(); if ($readerrolename && $readerrolecontext) { $managerroleid = $DB->get_field('role', 'id', ['shortname' => $readerrolename]); role_assign($managerroleid, $reader->id, $readerrolecontext->id); } $this->setUser($reader); } if ($expectexception) { // Setup if exception is expected for the test. $this->expectException(\moodle_exception::class); } external_api::clean_returnvalue( core_calendar_external::get_calendar_event_by_id_returns(), core_calendar_external::get_calendar_event_by_id($userevent->id) ); } /** * User data for testing editing or deleting calendar events. * * @return array */ public static function edit_or_delete_other_users_events_data_provider(): array { $syscontext = \context_system::instance(); $managerrole = 'manager'; return [ [false, false, $syscontext, $managerrole, false], [false, true, $syscontext, $managerrole, true], [false, false, null, null, true], [true, false, null, null, false], ]; } /** * Test the behavior of deleting other users' user events. * * @dataProvider edit_or_delete_other_users_events_data_provider * @covers \core_calendar_external::delete_calendar_events * @param bool $isadmin Whether the current user is admin. * @param bool $isadminevent Whether it's an admin event or not. * @param stdClass|null $writerrolecontext The reader role context. * @param string|null $writerrolename The role name. * @param bool $expectexception Whether the test should throw an exception or not. */ public function test_delete_other_users_events(bool $isadmin, bool $isadminevent, ?\stdClass $writerrolecontext, ?string $writerrolename, bool $expectexception): void { global $DB, $USER; $this->resetAfterTest(); $generator = $this->getDataGenerator(); if ($isadminevent) { $this->setAdminUser(); $user = $USER; } else { $user = $generator->create_user(); $this->setUser($user); } $userevent = $this->create_calendar_event('user event', $user->id, 'user', 0, time()); if ($isadmin) { $this->setAdminUser(); } else { $writer = $generator->create_user(); if ($writerrolename && $writerrolecontext) { $managerroleid = $DB->get_field('role', 'id', ['shortname' => $writerrolename]); role_assign($managerroleid, $writer->id, $writerrolecontext->id); } $this->setUser($writer); } if ($expectexception) { $this->expectException(\moodle_exception::class); } $events = [ ['eventid' => $userevent->id, 'repeat' => 0] ]; core_calendar_external::delete_calendar_events($events); } /** * Test the behavior of editing other users' user events * * @dataProvider edit_or_delete_other_users_events_data_provider * @covers \core_calendar_external::submit_create_update_form * @param bool $isadmin Whether the current user is admin. * @param bool $isadminevent Whether it's an admin event or not. * @param stdClass|null $writerrolecontext The reader role context. * @param string|null $writerrolename The role name. * @param bool $expectexception Whether the test should throw an exception or not. */ public function test_edit_other_users_events(bool $isadmin, bool $isadminevent, ?\stdClass $writerrolecontext, ?string $writerrolename, bool $expectexception): void { global $DB, $USER; $this->resetAfterTest(); $generator = $this->getDataGenerator(); if ($isadminevent) { $this->setAdminUser(); $user = $USER; } else { $user = $generator->create_user(); } $formdata = [ 'id' => 0, 'userid' => $user->id, 'modulename' => '', 'instance' => 0, 'visible' => 1, 'eventtype' => 'user', 'name' => 'Test', 'timestart' => [ 'day' => 1, 'month' => 1, 'year' => 2021, 'hour' => 1, 'minute' => 0, ], 'description' => [ 'text' => 'xxxxx', 'format' => 1, 'itemid' => 0 ], 'location' => 'Test', 'duration' => 0, ]; $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata); $querystring = http_build_query($formdata, '', '&'); if ($isadmin) { $this->setAdminUser(); } else { $writer = $generator->create_user(); if ($writerrolename && $writerrolecontext) { $managerroleid = $DB->get_field('role', 'id', ['shortname' => $writerrolename]); role_assign($managerroleid, $writer->id, $writerrolecontext->id); } $this->setUser($writer); } $USER->ignoresesskey = true; if ($expectexception) { $this->expectException(\moodle_exception::class); } core_calendar_external::submit_create_update_form($querystring); } /** * A user should not be able load the calendar events for a category they cannot see. */ public function test_get_calendar_events_hidden_category(): void { global $USER; $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user1 = $generator->create_user(); $category = $generator->create_category(['visible' => 0]); $name = 'Category Event (category: ' . $category->id . ')'; $record = new \stdClass(); $record->categoryid = $category->id; $categoryevent = $this->create_calendar_event($name, $USER->id, 'category', 0, time(), $record); $events = [ 'eventids' => [$categoryevent->id] ]; $options = []; // Admin can load the category event. $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_events_returns(), core_calendar_external::get_calendar_events($events, $options) ); $this->assertEquals($data['events'][0]['id'], $categoryevent->id); // User with no special permission to see hidden categories will not see the event. $this->setUser($user1); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_events_returns(), core_calendar_external::get_calendar_events($events, $options) ); $this->assertCount(0, $data['events']); $this->assertEquals('nopermissions', $data['warnings'][0]['warningcode']); } /** * Test get_calendar_access_information for admins. */ public function test_get_calendar_access_information_for_admins(): void { global $CFG; $this->resetAfterTest(true); $this->setAdminUser(); $CFG->calendar_adminseesall = 1; $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_access_information_returns(), core_calendar_external::get_calendar_access_information() ); $this->assertTrue($data['canmanageownentries']); $this->assertTrue($data['canmanagegroupentries']); $this->assertTrue($data['canmanageentries']); } /** * Test get_calendar_access_information for authenticated users. */ public function test_get_calendar_access_information_for_authenticated_users(): void { $this->resetAfterTest(true); $this->setUser($this->getDataGenerator()->create_user()); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_access_information_returns(), core_calendar_external::get_calendar_access_information() ); $this->assertTrue($data['canmanageownentries']); $this->assertFalse($data['canmanagegroupentries']); $this->assertFalse($data['canmanageentries']); } /** * Test get_calendar_access_information for student users. */ public function test_get_calendar_access_information_for_student_users(): void { global $DB; $this->resetAfterTest(true); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $role = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id); $this->setUser($user); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_access_information_returns(), core_calendar_external::get_calendar_access_information($course->id) ); $this->assertTrue($data['canmanageownentries']); $this->assertFalse($data['canmanagegroupentries']); $this->assertFalse($data['canmanageentries']); } /** * Test get_calendar_access_information for teacher users. */ public function test_get_calendar_access_information_for_teacher_users(): void { global $DB; $this->resetAfterTest(true); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(['groupmode' => 1]); $role = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id); $this->getDataGenerator()->create_group(['courseid' => $course->id]); $this->setUser($user); $data = external_api::clean_returnvalue( core_calendar_external::get_calendar_access_information_returns(), core_calendar_external::get_calendar_access_information($course->id) ); $this->assertTrue($data['canmanageownentries']); $this->assertTrue($data['canmanagegroupentries']); $this->assertTrue($data['canmanageentries']); } /** * Test get_allowed_event_types for admins. */ public function test_get_allowed_event_types_for_admins(): void { global $CFG; $this->resetAfterTest(true); $this->setAdminUser(); $CFG->calendar_adminseesall = 1; $data = external_api::clean_returnvalue( core_calendar_external::get_allowed_event_types_returns(), core_calendar_external::get_allowed_event_types() ); $this->assertEquals(['user', 'site', 'course', 'category'], $data['allowedeventtypes']); } /** * Test get_allowed_event_types for authenticated users. */ public function test_get_allowed_event_types_for_authenticated_users(): void { $this->resetAfterTest(true); $this->setUser($this->getDataGenerator()->create_user()); $data = external_api::clean_returnvalue( core_calendar_external::get_allowed_event_types_returns(), core_calendar_external::get_allowed_event_types() ); $this->assertEquals(['user'], $data['allowedeventtypes']); } /** * Test get_allowed_event_types for student users. */ public function test_get_allowed_event_types_for_student_users(): void { global $DB; $this->resetAfterTest(true); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $role = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id); $this->setUser($user); $data = external_api::clean_returnvalue( core_calendar_external::get_allowed_event_types_returns(), core_calendar_external::get_allowed_event_types($course->id) ); $this->assertEquals(['user'], $data['allowedeventtypes']); } /** * Test get_allowed_event_types for teacher users. */ public function test_get_allowed_event_types_for_teacher_users(): void { global $DB; $this->resetAfterTest(true); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(['groupmode' => 1]); $role = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id); $this->getDataGenerator()->create_group(['courseid' => $course->id]); $this->setUser($user); $data = external_api::clean_returnvalue( core_calendar_external::get_allowed_event_types_returns(), core_calendar_external::get_allowed_event_types($course->id) ); $this->assertEquals(['user', 'course', 'group'], $data['allowedeventtypes']); } /** * Test get_timestamps with string keys, with and without optional hour/minute values. */ public function test_get_timestamps_string_keys(): void { $this->resetAfterTest(true); $this->setAdminUser(); $time1 = new \DateTime('2018-12-30 00:00:00'); $time2 = new \DateTime('2019-03-27 23:59:00'); $dates = [ [ 'key' => 'from', 'year' => $time1->format('Y'), 'month' => $time1->format('m'), 'day' => $time1->format('d'), ], [ 'key' => 'to', 'year' => $time2->format('Y'), 'month' => (int) $time2->format('m'), 'day' => $time2->format('d'), 'hour' => $time2->format('H'), 'minute' => $time2->format('i'), ], ]; $expectedtimestamps = [ 'from' => $time1->getTimestamp(), 'to' => $time2->getTimestamp(), ]; $result = core_calendar_external::get_timestamps($dates); $this->assertEquals(['timestamps'], array_keys($result)); $this->assertEquals(2, count($result['timestamps'])); foreach ($result['timestamps'] as $data) { $this->assertTrue(in_array($data['key'], ['from', 'to'])); $this->assertEquals($expectedtimestamps[$data['key']], $data['timestamp']); } } /** * Test get_timestamps with no keys specified, with and without optional hour/minute values. */ public function test_get_timestamps_no_keys(): void { $this->resetAfterTest(true); $this->setAdminUser(); $time1 = new \DateTime('2018-12-30 00:00:00'); $time2 = new \DateTime('2019-03-27 23:59:00'); $dates = [ [ 'year' => $time1->format('Y'), 'month' => $time1->format('m'), 'day' => $time1->format('d'), ], [ 'year' => $time2->format('Y'), 'month' => (int) $time2->format('m'), 'day' => $time2->format('d'), 'hour' => $time2->format('H'), 'minute' => $time2->format('i'), ], ]; $expectedtimestamps = [ 0 => $time1->getTimestamp(), 1 => $time2->getTimestamp(), ]; $result = core_calendar_external::get_timestamps($dates); $this->assertEquals(['timestamps'], array_keys($result)); $this->assertEquals(2, count($result['timestamps'])); foreach ($result['timestamps'] as $data) { $this->assertEquals($expectedtimestamps[$data['key']], $data['timestamp']); } } /** * Test for checking if we receive the correct icon branding. * * @covers \core_calendar_external::get_calendar_action_events_by_timesort */ public function test_get_calendar_event_branded(): void { $this->resetAfterTest(true); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->setUser($user); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $assign = $generator->create_instance(['course' => $course->id]); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'instance' => $assign->id, 'courseid' => $course->id, 'modulename' => 'assign', 'timesort' => 1, ]; $event1 = $this->create_calendar_event('Event 1', $user->id, 'user', 0, 1, $params); $result = core_calendar_external::get_calendar_action_events_by_timesort(0); $result = external_api::clean_returnvalue( core_calendar_external::get_calendar_action_events_by_timesort_returns(), $result ); $this->assertCount(1, $result['events']); $this->assertFalse($result['events'][0]['branded']); } } PK*\G"**&tests/calendar_event_exporter_test.phpnu[. namespace core_calendar; use core_calendar\external\calendar_event_exporter; use core_calendar\local\event\container; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/helpers.php'); /** * Calendar event exporter testcase. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class calendar_event_exporter_test extends \advanced_testcase { /** * Data provider for the timestamp min limit test case to confirm * that the minimum time limit is set correctly on the boundary cases. */ public static function get_timestamp_min_limit_test_cases(): array { $now = time(); $todaymidnight = usergetmidnight($now); $tomorrowmidnight = $todaymidnight + DAYSECS; $eightam = $todaymidnight + (60 * 60 * 8); $starttime = (new \DateTime())->setTimestamp($eightam); return [ 'before min' => [ $starttime, [ ($starttime->getTimestamp() + 1), 'some error' ], $tomorrowmidnight ], 'equal min' => [ $starttime, [ $starttime->getTimestamp(), 'some error' ], $todaymidnight ], 'after min' => [ $starttime, [ ($starttime->getTimestamp() - 1), 'some error' ], $todaymidnight ] ]; } /** * @dataProvider get_timestamp_min_limit_test_cases */ public function test_get_timestamp_min_limit($starttime, $min, $expected): void { $class = calendar_event_exporter::class; $mock = $this->getMockBuilder($class) ->disableOriginalConstructor() ->onlyMethods([]) ->getMock(); $reflector = new \ReflectionClass($class); $method = $reflector->getMethod('get_timestamp_min_limit'); $result = $method->invoke($mock, $starttime, $min); $this->assertEquals($expected, $result['mindaytimestamp']); $this->assertEquals($min[1], $result['mindayerror']); } /** * Data provider for the timestamp max limit test case to confirm * that the maximum time limit is set correctly on the boundary cases. */ public static function get_timestamp_max_limit_test_cases(): array { $now = time(); $todaymidnight = usergetmidnight($now); $yesterdaymidnight = $todaymidnight - DAYSECS; $eightam = $todaymidnight + (60 * 60 * 8); $starttime = (new \DateTime())->setTimestamp($eightam); return [ 'before max' => [ $starttime, [ ($starttime->getTimestamp() + 1), 'some error' ], $todaymidnight ], 'equal max' => [ $starttime, [ $starttime->getTimestamp(), 'some error' ], $todaymidnight ], 'after max' => [ $starttime, [ ($starttime->getTimestamp() - 1), 'some error' ], $yesterdaymidnight ] ]; } /** * @dataProvider get_timestamp_max_limit_test_cases */ public function test_get_timestamp_max_limit($starttime, $max, $expected): void { $class = calendar_event_exporter::class; $mock = $this->getMockBuilder($class) ->disableOriginalConstructor() ->onlyMethods([]) ->getMock(); $reflector = new \ReflectionClass($class); $method = $reflector->getMethod('get_timestamp_max_limit'); $result = $method->invoke($mock, $starttime, $max); $this->assertEquals($expected, $result['maxdaytimestamp']); $this->assertEquals($max[1], $result['maxdayerror']); } /** * Exporting a course event should generate the course URL. */ public function test_calendar_event_exporter_course_url_course_event(): void { global $CFG, $PAGE; require_once($CFG->dirroot . '/course/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = \context_course::instance($course->id); $now = time(); $mapper = container::get_event_mapper(); $legacyevent = create_event([ 'courseid' => $course->id, 'userid' => 1, 'eventtype' => 'course', 'timestart' => $now ]); $event = $mapper->from_legacy_event_to_event($legacyevent); $exporter = new calendar_event_exporter($event, [ 'context' => $context, 'course' => $course, 'moduleinstance' => null, 'daylink' => new \moodle_url(''), 'type' => type_factory::get_calendar_instance(), 'today' => $now ]); $courseurl = course_get_url($course->id); $expected = $courseurl->out(false); $renderer = $PAGE->get_renderer('core_calendar'); $exportedevent = $exporter->export($renderer); // The exported URL should be for the course. $this->assertEquals($expected, $exportedevent->url); } /** * Exporting a user event should generate the site course URL. */ public function test_calendar_event_exporter_course_url_user_event(): void { global $CFG, $PAGE; require_once($CFG->dirroot . '/course/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $context = \context_user::instance($user->id); $now = time(); $mapper = container::get_event_mapper(); $legacyevent = create_event([ 'courseid' => 0, 'userid' => $user->id, 'eventtype' => 'user', 'timestart' => $now ]); $event = $mapper->from_legacy_event_to_event($legacyevent); $exporter = new calendar_event_exporter($event, [ 'context' => $context, 'course' => null, 'moduleinstance' => null, 'daylink' => new \moodle_url(''), 'type' => type_factory::get_calendar_instance(), 'today' => $now ]); $courseurl = course_get_url(SITEID); $expected = $courseurl->out(false); $renderer = $PAGE->get_renderer('core_calendar'); $exportedevent = $exporter->export($renderer); // The exported URL should be for the site course. $this->assertEquals($expected, $exportedevent->url); } /** * Popup name respects filters for course shortname. */ public function test_calendar_event_exporter_popupname_course_shortname_strips_links(): void { global $CFG, $PAGE; $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $rawshortname = 'Shortname link'; $nolinkshortname = strip_links($rawshortname); $course = $generator->create_course(['shortname' => $rawshortname]); $coursecontext = \context_course::instance($course->id); $now = time(); $mapper = container::get_event_mapper(); $renderer = $PAGE->get_renderer('core_calendar'); $legacyevent = create_event([ 'courseid' => $course->id, 'userid' => 1, 'eventtype' => 'course', 'timestart' => $now ]); $event = $mapper->from_legacy_event_to_event($legacyevent); $exporter = new calendar_event_exporter($event, [ 'context' => $coursecontext, 'course' => $course, 'moduleinstance' => null, 'daylink' => new \moodle_url(''), 'type' => type_factory::get_calendar_instance(), 'today' => $now ]); $exportedevent = $exporter->export($renderer); // Links should always be stripped from the course short name. $this->assertMatchesRegularExpression("/$nolinkshortname/", $exportedevent->popupname); } /** * Exported event contains the exported course. */ public function test_calendar_event_exporter_exports_course(): void { global $CFG, $PAGE; $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $rawshortname = 'Shortname link'; $nolinkshortname = strip_links($rawshortname); $course = $generator->create_course(['shortname' => $rawshortname]); $coursecontext = \context_course::instance($course->id); $now = time(); $mapper = container::get_event_mapper(); $renderer = $PAGE->get_renderer('core_calendar'); $legacyevent = create_event([ 'courseid' => $course->id, 'userid' => 1, 'eventtype' => 'course', 'timestart' => $now ]); $event = $mapper->from_legacy_event_to_event($legacyevent); $exporter = new calendar_event_exporter($event, [ 'context' => $coursecontext, 'course' => $course, 'moduleinstance' => null, 'daylink' => new \moodle_url(''), 'type' => type_factory::get_calendar_instance(), 'today' => $now ]); $exportedevent = $exporter->export($renderer); $courseexporter = new \core_course\external\course_summary_exporter($course, [ 'context' => $coursecontext ]); $exportedcourse = $courseexporter->export($renderer); $this->assertEquals($exportedevent->course, $exportedcourse); } } PK*\#Yk?~~tests/event/events_test.phpnu[. /** * This file contains the class that handles testing of the calendar events. * * @package core_calendar * @copyright 2014 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\event; use core_calendar_externallib_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/calendar/tests/externallib_test.php'); /** * This file contains the class that handles testing of the calendar events. * * @package core_calendar * @copyright 2014 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class events_test extends \advanced_testcase { /** * The test user. */ private $user; /** * The test course. */ private $course; /** * Test set up. */ protected function setUp(): void { global $USER; parent::setUp(); // The user we are going to test this on. $this->setAdminUser(); $this->user = $USER; $this->course = self::getDataGenerator()->create_course(); } /** * Tests for calendar_event_created event. */ public function test_calendar_event_created(): void { $this->resetAfterTest(); // Catch the events. $sink = $this->redirectEvents(); // Create a calendar event. $record = new \stdClass(); $record->courseid = 0; $time = time(); $calevent = \core_calendar\externallib_test::create_calendar_event('event', $this->user->id, 'user', 0, $time, $record); // User event. // Capture the event. $events = $sink->get_events(); $sink->clear(); // Validate the event. $event = $events[0]; $this->assertInstanceOf('\core\event\calendar_event_created', $event); $this->assertEquals('event', $event->objecttable); $this->assertEquals(0, $event->courseid); $this->assertEquals($calevent->context, $event->get_context()); $other = array('repeatid' => 0, 'timestart' => $time, 'name' => 'event'); $this->assertEquals($other, $event->other); $this->assertEventContextNotUsed($event); // Now we create a repeated course event. $record = new \stdClass(); $record->courseid = $this->course->id; $calevent = \core_calendar\externallib_test::create_calendar_event('course', $this->user->id, 'course', 10, $time, $record); $events = $sink->get_events(); $sink->close(); $this->assertEquals(10, count($events)); foreach ($events as $event) { $this->assertInstanceOf('\core\event\calendar_event_created', $event); $this->assertEquals('event', $event->objecttable); $this->assertEquals($this->course->id, $event->courseid); $this->assertEquals($calevent->context, $event->get_context()); } } /** * Tests for event validations related to calendar_event_created event. */ public function test_calendar_event_created_validations(): void { $this->resetAfterTest(); $context = \context_user::instance($this->user->id); // Test not setting other['repeatid']. try { \core\event\calendar_event_created::create(array( 'context' => $context, 'objectid' => 2, 'other' => array( 'timestart' => time(), 'name' => 'event' ) )); $this->fail("Event validation should not allow \\core\\event\\calendar_event_created to be triggered without other['repeatid']"); } catch (\coding_exception $e) { $this->assertStringContainsString('The \'repeatid\' value must be set in other.', $e->getMessage()); } // Test not setting other['name']. try { \core\event\calendar_event_created::create(array( 'context' => $context, 'objectid' => 2, 'other' => array( 'repeatid' => 0, 'timestart' => time(), ) )); $this->fail("Event validation should not allow \\core\\event\\calendar_event_created to be triggered without other['name']"); } catch (\coding_exception $e) { $this->assertStringContainsString('The \'name\' value must be set in other.', $e->getMessage()); } // Test not setting other['timestart']. try { \core\event\calendar_event_created::create(array( 'context' => $context, 'objectid' => 2, 'other' => array( 'name' => 'event', 'repeatid' => 0, ) )); $this->fail("Event validation should not allow \\core\\event\\calendar_event_deleted to be triggered without other['timestart']"); } catch (\coding_exception $e) { $this->assertStringContainsString('The \'timestart\' value must be set in other.', $e->getMessage()); } } /** * Tests for calendar_event_updated event. */ public function test_calendar_event_updated(): void { $this->resetAfterTest(); // Create a calendar event. $record = new \stdClass(); $record->courseid = 0; $time = time(); $calevent = \core_calendar\externallib_test::create_calendar_event('event', $this->user->id, 'user', 0, $time, $record); // User event. // Catch the events. $sink = $this->redirectEvents(); $prop = new \stdClass(); $prop->name = 'new event'; $calevent->update($prop); // Update calender event. // Capture the event. $events = $sink->get_events(); // Validate the event. $event = $events[0]; $this->assertInstanceOf('\core\event\calendar_event_updated', $event); $this->assertEquals('event', $event->objecttable); $this->assertEquals(0, $event->courseid); $this->assertEquals($calevent->context, $event->get_context()); $other = array('repeatid' => 0, 'timestart' => $time, 'name' => 'new event'); $this->assertEquals($other, $event->other); $this->assertEventContextNotUsed($event); // Now we create a repeated course event and update it. $record = new \stdClass(); $record->courseid = $this->course->id; $calevent = \core_calendar\externallib_test::create_calendar_event('course', $this->user->id, 'course', 10, time(), $record); $sink->clear(); $prop = new \stdClass(); $prop->name = 'new event'; $prop->repeateditall = true; $calevent->update($prop); // Update calender event. $events = $sink->get_events(); $sink->close(); $this->assertEquals(10, count($events)); foreach ($events as $event) { $this->assertInstanceOf('\core\event\calendar_event_updated', $event); $this->assertEquals('event', $event->objecttable); $this->assertEquals($this->course->id, $event->courseid); $this->assertEquals($calevent->context, $event->get_context()); } } /** * Tests for calendar_event_updated event. */ public function test_calendar_event_updated_toggle_visibility(): void { global $DB; $siteid = 0; $this->resetAfterTest(); // Create a calendar event. $time = time(); $calevent = \core_calendar\externallib_test::create_calendar_event('Some wickedly awesome event yo!', $this->user->id, 'user', 0, $time); // Updated the visibility of the calendar event. $sink = $this->redirectEvents(); $calevent->toggle_visibility(); $dbrecord = $DB->get_record('event', array('id' => $calevent->id), '*', MUST_EXIST); $events = $sink->get_events(); // Validate the calendar_event_updated event. $event = $events[0]; $this->assertInstanceOf('\core\event\calendar_event_updated', $event); $this->assertEquals('event', $event->objecttable); $this->assertEquals($siteid, $event->courseid); $this->assertEquals($calevent->context, $event->get_context()); $other = array('repeatid' => 0, 'timestart' => $time, 'name' => 'Some wickedly awesome event yo!'); $this->assertEquals($other, $event->other); $this->assertEventContextNotUsed($event); $this->assertEquals($dbrecord, $event->get_record_snapshot('event', $event->objectid)); } /** * Tests for event validations related to calendar_event_created event. */ public function test_calendar_event_updated_validations(): void { $this->resetAfterTest(); $context = \context_user::instance($this->user->id); // Test not setting other['repeatid']. try { \core\event\calendar_event_updated::create(array( 'context' => $context, 'objectid' => 2, 'other' => array( 'timestart' => time(), 'name' => 'event' ) )); $this->fail("Event validation should not allow \\core\\event\\calendar_event_updated to be triggered without other['repeatid']"); } catch (\coding_exception $e) { $this->assertStringContainsString('The \'repeatid\' value must be set in other.', $e->getMessage()); } // Test not setting other['name']. try { \core\event\calendar_event_updated::create(array( 'context' => $context, 'objectid' => 2, 'other' => array( 'repeatid' => 0, 'timestart' => time(), ) )); $this->fail("Event validation should not allow \\core\\event\\calendar_event_updated to be triggered without other['name']"); } catch (\coding_exception $e) { $this->assertStringContainsString('The \'name\' value must be set in other.', $e->getMessage()); } // Test not setting other['timestart']. try { \core\event\calendar_event_updated::create(array( 'context' => $context, 'objectid' => 2, 'other' => array( 'name' => 'event', 'repeatid' => 0, ) )); $this->fail("Event validation should not allow \\core\\event\\calendar_event_deleted to be triggered without other['timestart']"); } catch (\coding_exception $e) { $this->assertStringContainsString('The \'timestart\' value must be set in other.', $e->getMessage()); } } /** * Tests for calendar_event_deleted event. */ public function test_calendar_event_deleted(): void { global $DB; $this->resetAfterTest(); // Create a calendar event. $record = new \stdClass(); $record->courseid = 0; $record->repeatid = 0; $time = time(); $calevent = \core_calendar\externallib_test::create_calendar_event('event', $this->user->id, 'user', 0, $time, $record); // User event. $dbrecord = $DB->get_record('event', array('id' => $calevent->id), '*', MUST_EXIST); // Catch the events. $sink = $this->redirectEvents(); $calevent->delete(false); $events = $sink->get_events(); // Validate the event. $event = $events[0]; $this->assertInstanceOf('\core\event\calendar_event_deleted', $event); $this->assertEquals('event', $event->objecttable); $this->assertEquals(0, $event->courseid); $this->assertEquals($calevent->context, $event->get_context()); $other = array('repeatid' => 0, 'timestart' => $time, 'name' => 'event'); $this->assertEquals($other, $event->other); $this->assertEventContextNotUsed($event); $this->assertEquals($dbrecord, $event->get_record_snapshot('event', $event->objectid)); // Now we create a repeated course event and delete it. $record = new \stdClass(); $record->courseid = $this->course->id; $calevent = \core_calendar\externallib_test::create_calendar_event('course', $this->user->id, 'course', 10, time(), $record); $sink->clear(); $prop = new \stdClass(); $prop->name = 'new event'; $prop->repeateditall = true; $calevent->delete(true); $events = $sink->get_events(); $sink->close(); $this->assertEquals(10, count($events)); foreach ($events as $event) { $this->assertInstanceOf('\core\event\calendar_event_deleted', $event); $this->assertEquals('event', $event->objecttable); $this->assertEquals($this->course->id, $event->courseid); $this->assertEquals($calevent->context, $event->get_context()); } } /** * Tests for event validations related to calendar_event_deleted event. */ public function test_calendar_event_deleted_validations(): void { $this->resetAfterTest(); $context = \context_user::instance($this->user->id); // Test not setting other['repeatid']. try { \core\event\calendar_event_deleted::create(array( 'context' => $context, 'objectid' => 2, 'other' => array( 'timestart' => time(), 'name' => 'event' ) )); $this->fail("Event validation should not allow \\core\\event\\calendar_event_deleted to be triggered without other['repeatid']"); } catch (\coding_exception $e) { $this->assertStringContainsString('The \'repeatid\' value must be set in other.', $e->getMessage()); } // Test not setting other['name']. try { \core\event\calendar_event_deleted::create(array( 'context' => $context, 'objectid' => 2, 'other' => array( 'repeatid' => 0, 'timestart' => time(), ) )); $this->fail("Event validation should not allow \\core\\event\\calendar_event_deleted to be triggered without other['name']"); } catch (\coding_exception $e) { $this->assertStringContainsString('The \'name\' value must be set in other.', $e->getMessage()); } // Test not setting other['timestart']. try { \core\event\calendar_event_deleted::create(array( 'context' => $context, 'objectid' => 2, 'other' => array( 'name' => 'event', 'repeatid' => 0, ) )); $this->fail("Event validation should not allow \\core\\event\\calendar_event_deleted to be triggered without other['timestart']"); } catch (\coding_exception $e) { $this->assertStringContainsString('The \'timestart\' value must be set in other.', $e->getMessage()); } } /** * Tests for calendar_subscription_added event for a site subscription. */ public function test_calendar_subscription_created_site(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); // Create a mock subscription. $subscription = new \stdClass(); $subscription->eventtype = 'site'; $subscription->name = 'test'; $subscription->courseid = $this->course->id; // Trigger and capture the event. $sink = $this->redirectEvents(); $id = calendar_add_subscription($subscription); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\calendar_subscription_created', $event); $this->assertEquals($id, $event->objectid); $this->assertEquals($subscription->courseid, $event->other['courseid']); $this->assertEquals($subscription->eventtype, $event->other['eventtype']); $this->assertArrayNotHasKey('categoryid', $event->other); $this->assertArrayNotHasKey('groupid', $event->other); $this->assertDebuggingNotCalled(); $sink->close(); } /** * Tests for calendar_subscription_added event for a category subscription. */ public function test_calendar_subscription_created_category(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); $categoryid = $this->course->category; // Create a mock subscription. $subscription = new \stdClass(); $subscription->eventtype = 'category'; $subscription->name = 'test'; $subscription->categoryid = $categoryid; // Trigger and capture the event. $sink = $this->redirectEvents(); $id = calendar_add_subscription($subscription); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\calendar_subscription_created', $event); $this->assertEquals($id, $event->objectid); $this->assertEquals($categoryid, $event->other['categoryid']); $this->assertEquals($subscription->eventtype, $event->other['eventtype']); $this->assertArrayNotHasKey('courseid', $event->other); $this->assertArrayNotHasKey('groupid', $event->other); $this->assertDebuggingNotCalled(); $sink->close(); } /** * Tests for calendar_subscription_added event for a course subscription. */ public function test_calendar_subscription_created_course(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); // Create a mock subscription. $subscription = new \stdClass(); $subscription->eventtype = 'course'; $subscription->name = 'test'; $subscription->courseid = $this->course->id; // Trigger and capture the event. $sink = $this->redirectEvents(); $id = calendar_add_subscription($subscription); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\calendar_subscription_created', $event); $this->assertEquals($id, $event->objectid); $this->assertEquals($subscription->courseid, $event->other['courseid']); $this->assertEquals($subscription->eventtype, $event->other['eventtype']); $this->assertArrayNotHasKey('categoryid', $event->other); $this->assertArrayNotHasKey('groupid', $event->other); $this->assertDebuggingNotCalled(); $sink->close(); } /** * Tests for calendar_subscription_added event for a group subscription. */ public function test_calendar_subscription_created_group(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); $courseid = $this->course->id; $groupid = 42; // Create a mock subscription. $subscription = new \stdClass(); $subscription->eventtype = 'group'; $subscription->name = 'test'; $subscription->courseid = $courseid; $subscription->groupid = $groupid; // Trigger and capture the event. $sink = $this->redirectEvents(); $id = calendar_add_subscription($subscription); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\calendar_subscription_created', $event); $this->assertEquals($id, $event->objectid); $this->assertEquals($courseid, $event->other['courseid']); $this->assertEquals($groupid, $event->other['groupid']); $this->assertEquals($subscription->eventtype, $event->other['eventtype']); $this->assertArrayNotHasKey('categoryid', $event->other); $this->assertDebuggingNotCalled(); $sink->close(); } /** * Tests for calendar_subscription_updated event for a site subscription. */ public function test_calendar_subscription_updated_site(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); // Create a mock subscription. $subscription = new \stdClass(); $subscription->eventtype = 'site'; $subscription->name = 'test'; $subscription->courseid = $this->course->id; $subscription->id = calendar_add_subscription($subscription); // Now edit it. $subscription->name = 'awesome'; // Trigger and capture the event. $sink = $this->redirectEvents(); calendar_update_subscription($subscription); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\calendar_subscription_updated', $event); $this->assertEquals($subscription->id, $event->objectid); $this->assertEquals($subscription->courseid, $event->other['courseid']); $this->assertEquals($subscription->eventtype, $event->other['eventtype']); $this->assertArrayNotHasKey('categoryid', $event->other); $this->assertArrayNotHasKey('groupid', $event->other); $this->assertDebuggingNotCalled(); $sink->close(); } /** * Tests for calendar_subscription_updated event for a category subscription. */ public function test_calendar_subscription_updated_category(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); $categoryid = $this->course->category; // Create a mock subscription. $subscription = new \stdClass(); $subscription->eventtype = 'category'; $subscription->name = 'test'; $subscription->categoryid = $categoryid; $subscription->id = calendar_add_subscription($subscription); // Now edit it. $subscription->name = 'awesome'; // Trigger and capture the event. $sink = $this->redirectEvents(); calendar_update_subscription($subscription); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\calendar_subscription_updated', $event); $this->assertEquals($subscription->id, $event->objectid); $this->assertEquals($categoryid, $event->other['categoryid']); $this->assertEquals($subscription->eventtype, $event->other['eventtype']); $this->assertArrayNotHasKey('courseid', $event->other); $this->assertArrayNotHasKey('groupid', $event->other); $this->assertDebuggingNotCalled(); $sink->close(); } /** * Tests for calendar_subscription_updated event for a group subscription. */ public function test_calendar_subscription_updated_course(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); // Create a mock subscription. $subscription = new \stdClass(); $subscription->eventtype = 'course'; $subscription->name = 'test'; $subscription->courseid = $this->course->id; $subscription->id = calendar_add_subscription($subscription); // Now edit it. $subscription->name = 'awesome'; // Trigger and capture the event. $sink = $this->redirectEvents(); calendar_update_subscription($subscription); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\calendar_subscription_updated', $event); $this->assertEquals($subscription->id, $event->objectid); $this->assertEquals($this->course->id, $event->other['courseid']); $this->assertEquals($subscription->eventtype, $event->other['eventtype']); $this->assertArrayNotHasKey('categoryid', $event->other); $this->assertArrayNotHasKey('groupid', $event->other); $this->assertDebuggingNotCalled(); $sink->close(); } /** * Tests for calendar_subscription_updated event for a course subscription. */ public function test_calendar_subscription_updated_group(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); $courseid = $this->course->id; $groupid = 42; // Create a mock subscription. $subscription = new \stdClass(); $subscription->eventtype = 'group'; $subscription->name = 'test'; $subscription->courseid = $courseid; $subscription->groupid = $groupid; $subscription->id = calendar_add_subscription($subscription); // Now edit it. $subscription->name = 'awesome'; // Trigger and capture the event. $sink = $this->redirectEvents(); calendar_update_subscription($subscription); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\calendar_subscription_updated', $event); $this->assertEquals($subscription->id, $event->objectid); $this->assertEquals($this->course->id, $event->other['courseid']); $this->assertEquals($groupid, $event->other['groupid']); $this->assertEquals($subscription->eventtype, $event->other['eventtype']); $this->assertArrayNotHasKey('categoryid', $event->other); $this->assertDebuggingNotCalled(); $sink->close(); } /** * Tests for calendar_subscription_deleted event for a site subscription. */ public function test_calendar_subscription_deleted_site(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); // Create a mock subscription. $subscription = new \stdClass(); $subscription->eventtype = 'site'; $subscription->name = 'test'; $subscription->courseid = $this->course->id; $subscription->id = calendar_add_subscription($subscription); // Trigger and capture the event. $sink = $this->redirectEvents(); calendar_delete_subscription($subscription); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event); $this->assertEquals($subscription->id, $event->objectid); $this->assertEquals($subscription->courseid, $event->other['courseid']); $this->assertDebuggingNotCalled(); $sink->close(); } /** * Tests for calendar_subscription_deleted event for a category subscription. */ public function test_calendar_subscription_deleted_category(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); $categoryid = $this->course->category; // Create a mock subscription. $subscription = new \stdClass(); $subscription->eventtype = 'category'; $subscription->name = 'test'; $subscription->categoryid = $categoryid; $subscription->id = calendar_add_subscription($subscription); // Trigger and capture the event. $sink = $this->redirectEvents(); calendar_delete_subscription($subscription); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event); $this->assertEquals($subscription->id, $event->objectid); $this->assertEquals($categoryid, $event->other['categoryid']); $this->assertEquals($subscription->eventtype, $event->other['eventtype']); $this->assertArrayNotHasKey('courseid', $event->other); $this->assertArrayNotHasKey('groupid', $event->other); $this->assertDebuggingNotCalled(); $sink->close(); } /** * Tests for calendar_subscription_deleted event for a course. */ public function test_calendar_subscription_deleted_course(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); // Create a mock subscription. $subscription = new \stdClass(); $subscription->eventtype = 'course'; $subscription->name = 'test'; $subscription->courseid = $this->course->id; $subscription->id = calendar_add_subscription($subscription); // Trigger and capture the event. $sink = $this->redirectEvents(); calendar_delete_subscription($subscription); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event); $this->assertEquals($subscription->id, $event->objectid); $this->assertEquals($this->course->id, $event->other['courseid']); $this->assertEquals($subscription->eventtype, $event->other['eventtype']); $this->assertArrayNotHasKey('categoryid', $event->other); $this->assertArrayNotHasKey('groupid', $event->other); $this->assertDebuggingNotCalled(); $sink->close(); } /** * Tests for calendar_subscription_deleted event for a group. */ public function test_calendar_subscription_deleted_group(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); $courseid = $this->course->id; $groupid = 42; // Create a mock subscription. $subscription = new \stdClass(); $subscription->eventtype = 'group'; $subscription->name = 'test'; $subscription->groupid = $groupid; $subscription->courseid = $courseid; $subscription->id = calendar_add_subscription($subscription); // Trigger and capture the event. $sink = $this->redirectEvents(); calendar_delete_subscription($subscription); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event); $this->assertEquals($subscription->id, $event->objectid); $this->assertEquals($this->course->id, $event->other['courseid']); $this->assertEquals($groupid, $event->other['groupid']); $this->assertEquals($subscription->eventtype, $event->other['eventtype']); $this->assertArrayNotHasKey('categoryid', $event->other); $this->assertDebuggingNotCalled(); $sink->close(); } } PK*\K{pptests/cm_info_proxy_test.phpnu[. namespace core_calendar; use core_calendar\local\event\proxies\cm_info_proxy; /** * cm_info_proxy testcase. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class cm_info_proxy_test extends \advanced_testcase { /** * Test creating cm_info_std_proxy, using getter and setter. */ public function test_proxy(): void { $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $module = $this->getDataGenerator()->create_module('forum', ['course' => $course->id, 'idnumber' => '123456']); $proxy = new cm_info_proxy( 'forum', $module->id, $course->id ); $this->assertEquals('forum', $proxy->get('modname')); $this->assertEquals($module->id, $proxy->get('instance')); $this->assertEquals($course->id, $proxy->get('course')); $this->assertEquals('123456', $proxy->get('idnumber')); $this->assertEquals($module->cmid, $proxy->get('id')); $this->assertEquals('123456', $proxy->get_proxied_instance()->idnumber); } } PK*\F%44tests/event_mapper_test.phpnu[. /** * Event mapper test. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar; use core_calendar\local\event\mappers\event_mapper; use core_calendar\local\event\value_objects\action; use core_calendar\local\event\value_objects\event_description; use core_calendar\local\event\value_objects\event_times; use core_calendar\local\event\factories\action_factory_interface; use core_calendar\local\event\entities\event_collection_interface; use core_calendar\local\event\factories\event_factory_interface; use core_calendar\local\event\entities\event_interface; use core_calendar\local\event\entities\action_event_interface; use core_calendar\local\event\proxies\proxy_interface; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); /** * Event mapper test. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class event_mapper_test extends \advanced_testcase { /** * Test legacy event -> event. */ public function test_from_legacy_event_to_event(): void { $this->resetAfterTest(true); $this->setAdminUser(); $legacyevent = $this->create_event(); $mapper = new event_mapper( new event_mapper_test_event_factory() ); $event = $mapper->from_legacy_event_to_event($legacyevent); $this->assertInstanceOf(event_interface::class, $event); } /** * Test event -> legacy event. */ public function test_from_event_to_legacy_event(): void { $this->resetAfterTest(true); $this->setAdminUser(); $legacyevent = $this->create_event(['modname' => 'assign', 'instance' => 1]); $event = new event_mapper_test_event($legacyevent); $mapper = new event_mapper( new event_mapper_test_event_factory() ); $legacyevent = $mapper->from_event_to_legacy_event($event); $this->assertInstanceOf(\calendar_event::class, $legacyevent); } /** * Test event -> stdClass. */ public function test_from_event_to_stdclass(): void { $this->resetAfterTest(true); $this->setAdminUser(); $legacyevent = $this->create_event(['modname' => 'assign', 'instance' => 1]); $event = new event_mapper_test_event($legacyevent); $mapper = new event_mapper( new event_mapper_test_event_factory() ); $obj = $mapper->from_event_to_stdClass($event); $this->assertInstanceOf(\stdClass::class, $obj); $this->assertEquals($obj->name, $event->get_name()); $this->assertEquals($obj->eventtype, $event->get_type()); $this->assertEquals($obj->timestart, $event->get_times()->get_start_time()->getTimestamp()); } /** * Test event -> array. */ public function test_from_event_to_assoc_array(): void { $this->resetAfterTest(true); $this->setAdminUser(); $legacyevent = $this->create_event(['modname' => 'assign', 'instance' => 1]); $event = new event_mapper_test_event($legacyevent); $mapper = new event_mapper( new event_mapper_test_event_factory() ); $arr = $mapper->from_event_to_assoc_array($event); $this->assertTrue(is_array($arr)); $this->assertEquals($arr['name'], $event->get_name()); $this->assertEquals($arr['eventtype'], $event->get_type()); $this->assertEquals($arr['timestart'], $event->get_times()->get_start_time()->getTimestamp()); } /** * Test for action event -> legacy event. */ public function test_from_action_event_to_legacy_event(): void { $this->resetAfterTest(true); $this->setAdminUser(); $legacyevent = $this->create_event(['modname' => 'assign', 'instance' => 1]); $event = new event_mapper_test_action_event( new event_mapper_test_event($legacyevent) ); $mapper = new event_mapper( new event_mapper_test_event_factory() ); $legacyevent = $mapper->from_event_to_legacy_event($event); $this->assertInstanceOf(\calendar_event::class, $legacyevent); $this->assertEquals($legacyevent->actionname, 'test action'); $this->assertInstanceOf(\moodle_url::class, $legacyevent->actionurl); $this->assertEquals($legacyevent->actionnum, 1729); $this->assertEquals($legacyevent->actionactionable, $event->get_action()->is_actionable()); } /** * Helper function to create calendar events using the old code. * * @param array $properties A list of calendar event properties to set * @return calendar_event */ protected function create_event($properties = []) { $record = new \stdClass(); $record->name = 'event name'; $record->eventtype = 'site'; $record->timestart = time(); $record->timeduration = 0; $record->timesort = 0; $record->type = 1; $record->courseid = 0; $record->categoryid = 0; foreach ($properties as $name => $value) { $record->$name = $value; } $event = new \calendar_event($record); return $event->create($record, false); } } /** * A test event factory. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_mapper_test_event_factory implements event_factory_interface { public function create_instance(\stdClass $dbrow) { return new event_mapper_test_event(); } } /** * A test action event * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_mapper_test_action_event implements action_event_interface { /** * @var event_interface $event The event to delegate to. */ protected $event; /** * event_mapper_test_action_event constructor. * @param event_interface $event */ public function __construct(event_interface $event) { $this->event = $event; } public function get_id() { return $this->event->get_id(); } public function get_name() { return $this->event->get_name(); } public function get_description() { return $this->event->get_description(); } public function get_location() { return $this->event->get_location(); } public function get_category() { return $this->event->get_category(); } public function get_course() { return $this->event->get_course(); } public function get_course_module() { return $this->event->get_course_module(); } public function get_group() { return $this->event->get_group(); } public function get_user() { return $this->event->get_user(); } public function get_type() { return $this->event->get_type(); } public function get_times() { return $this->event->get_times(); } public function get_repeats() { return $this->event->get_repeats(); } public function get_subscription() { return $this->event->get_subscription(); } public function is_visible() { return $this->event->is_visible(); } public function get_action() { return new action( 'test action', new \moodle_url('http://example.com'), 1729, true ); } /** * Component * @return string|null */ public function get_component() { return $this->event->get_component(); } } /** * A test event. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_mapper_test_event implements event_interface { /** * @var proxy_interface $categoryproxy Category proxy. */ protected $categoryproxy; /** * @var proxy_interface $courseproxy Course proxy. */ protected $courseproxy; /** * @var proxy_interface $cmproxy Course module proxy. */ protected $cmproxy; /** * @var proxy_interface $groupproxy Group proxy. */ protected $groupproxy; /** * @var proxy_interface $userproxy User proxy. */ protected $userproxy; /** * @var proxy_interface $subscriptionproxy Subscription proxy. */ protected $subscriptionproxy; /** * Constructor. * * @param calendar_event $legacyevent Legacy event to extract IDs etc from. */ public function __construct($legacyevent = null) { if ($legacyevent) { $this->courseproxy = new event_mapper_test_proxy($legacyevent->courseid); $this->cmproxy = new event_mapper_test_proxy(1729, [ 'modname' => $legacyevent->modname, 'instance' => $legacyevent->instance ] ); $this->groupproxy = new event_mapper_test_proxy(0); $this->userproxy = new event_mapper_test_proxy($legacyevent->userid); $this->subscriptionproxy = new event_mapper_test_proxy(null); } } public function get_id() { return 1729; } public function get_name() { return 'Jeff'; } public function get_description() { return new event_description('asdf', 1); } public function get_location() { return 'Cube office'; } public function get_category() { return $this->categoryproxy; } public function get_course() { return $this->courseproxy; } public function get_course_module() { return $this->cmproxy; } public function get_group() { return $this->groupproxy; } public function get_user() { return $this->userproxy; } public function get_type() { return 'asdf'; } public function get_times() { return new event_times( (new \DateTimeImmutable())->setTimestamp(-386380800), (new \DateTimeImmutable())->setTimestamp(115776000), (new \DateTimeImmutable())->setTimestamp(115776000), (new \DateTimeImmutable())->setTimestamp(time()), (new \DateTimeImmutable())->setTimestamp(115776000) ); } public function get_repeats() { return new core_calendar_event_mapper_test_event_collection(); } public function get_subscription() { return $this->subscriptionproxy; } public function is_visible() { return true; } /** * Component * @return string|null */ public function get_component() { return null; } } /** * A test proxy. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_mapper_test_proxy implements proxy_interface { /** * @var int $id Proxied ID. */ protected $id; /** * @var array $params Params to proxy. */ protected $params; /** * Constructor. * * @param int $id Proxied ID. * @param array $params Params to proxy. */ public function __construct($id, $params = []) { $this->params = $params; } public function get($member) { if ($member === 'id') { return $this->id; } return isset($params[$member]) ? $params[$member] : null; } public function get_proxied_instance() { } } /** * A test event. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_calendar_event_mapper_test_event_collection implements event_collection_interface { /** * @var array $events Array of events. */ protected $events; /** * Constructor. */ public function __construct() { $this->events = [ 'not really an event hahaha', 'also not really. gottem.' ]; } public function get_id() { return 1729; } public function get_num() { return 2; } public function getIterator(): \Traversable { foreach ($this->events as $event) { yield $event; } } } PK*\$ltests/event_vault_test.phpnu[. namespace core_calendar; use action_event_test_factory; use core_calendar\local\event\data_access\event_vault; use core_calendar\local\event\strategies\raw_event_retrieval_strategy; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/calendar/tests/helpers.php'); /** * This file contains the class that handles testing of the calendar event vault. * * @package core_calendar * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class event_vault_test extends \advanced_testcase { /** * Test that get_action_events_by_timesort returns events after the * provided timesort value. */ public function test_get_action_events_by_timesort_after_time(): void { $this->resetAfterTest(true); $user = $this->getDataGenerator()->create_user(); $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); $this->setUser($user); for ($i = 1; $i < 6; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION ]); } $events = $vault->get_action_events_by_timesort($user, 3); $this->assertCount(3, $events); $this->assertEquals('Event 3', $events[0]->get_name()); $this->assertEquals('Event 4', $events[1]->get_name()); $this->assertEquals('Event 5', $events[2]->get_name()); $events = $vault->get_action_events_by_timesort($user, 3, null, null, 1); $this->assertCount(1, $events); $this->assertEquals('Event 3', $events[0]->get_name()); $events = $vault->get_action_events_by_timesort($user, 6); $this->assertCount(0, $events); } /** * Test that get_action_events_by_timesort returns events before the * provided timesort value. */ public function test_get_action_events_by_timesort_before_time(): void { $this->resetAfterTest(true); $this->setAdminuser(); $user = $this->getDataGenerator()->create_user(); $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); for ($i = 1; $i < 6; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => 1 ]); } $events = $vault->get_action_events_by_timesort($user, null, 3); $this->assertCount(3, $events); $this->assertEquals('Event 1', $events[0]->get_name()); $this->assertEquals('Event 2', $events[1]->get_name()); $this->assertEquals('Event 3', $events[2]->get_name()); $events = $vault->get_action_events_by_timesort($user, null, 3, null, 1); $this->assertCount(1, $events); $this->assertEquals('Event 1', $events[0]->get_name()); $events = $vault->get_action_events_by_timesort($user, 6); $this->assertCount(0, $events); } /** * Test that get_action_events_by_timesort returns events between the * provided timesort values. */ public function test_get_action_events_by_timesort_between_time(): void { $this->resetAfterTest(true); $this->setAdminuser(); $user = $this->getDataGenerator()->create_user(); $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); for ($i = 1; $i < 6; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => 1 ]); } $events = $vault->get_action_events_by_timesort($user, 2, 4); $this->assertCount(3, $events); $this->assertEquals('Event 2', $events[0]->get_name()); $this->assertEquals('Event 3', $events[1]->get_name()); $this->assertEquals('Event 4', $events[2]->get_name()); $events = $vault->get_action_events_by_timesort($user, 2, 4, null, 1); $this->assertCount(1, $events); $this->assertEquals('Event 2', $events[0]->get_name()); } /** * Test that get_action_events_by_timesort returns events between the * provided timesort values and after the last seen event when one is * provided. */ public function test_get_action_events_by_timesort_between_time_after_event(): void { $this->resetAfterTest(true); $this->setAdminuser(); $user = $this->getDataGenerator()->create_user(); $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); $records = []; for ($i = 1; $i < 21; $i++) { $records[] = create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => 1 ]); } $aftereventid = $records[6]->id; $afterevent = $vault->get_event_by_id($aftereventid); $events = $vault->get_action_events_by_timesort($user, 3, 15, $afterevent); $this->assertCount(8, $events); $this->assertEquals('Event 8', $events[0]->get_name()); $events = $vault->get_action_events_by_timesort($user, 3, 15, $afterevent, 3); $this->assertCount(3, $events); } /** * Test that get_action_events_by_timesort returns events between the * provided timesort values and the last seen event can be provided to * get paginated results. */ public function test_get_action_events_by_timesort_between_time_skip_even_records(): void { $this->resetAfterTest(true); $this->setAdminuser(); $user = $this->getDataGenerator()->create_user(); // The factory will return every event that is divisible by 2. $factory = new action_event_test_factory(function($actionevent) { static $count = 0; $count++; return ($count % 2) ? true : false; }); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); for ($i = 1; $i < 41; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => 1 ]); } $events = $vault->get_action_events_by_timesort($user, 3, 35, null, 5); $this->assertCount(5, $events); $this->assertEquals('Event 3', $events[0]->get_name()); $this->assertEquals('Event 5', $events[1]->get_name()); $this->assertEquals('Event 7', $events[2]->get_name()); $this->assertEquals('Event 9', $events[3]->get_name()); $this->assertEquals('Event 11', $events[4]->get_name()); $afterevent = $events[4]; $events = $vault->get_action_events_by_timesort($user, 3, 35, $afterevent, 5); $this->assertCount(5, $events); $this->assertEquals('Event 13', $events[0]->get_name()); $this->assertEquals('Event 15', $events[1]->get_name()); $this->assertEquals('Event 17', $events[2]->get_name()); $this->assertEquals('Event 19', $events[3]->get_name()); $this->assertEquals('Event 21', $events[4]->get_name()); } /** * Test that get_action_events_by_timesort returns events between the * provided timesort values. The database will continue to be read until the * number of events requested has been satisfied. In this case the first * five events are rejected so it should require two database requests. */ public function test_get_action_events_by_timesort_between_time_skip_first_records(): void { $this->resetAfterTest(true); $this->setAdminuser(); $user = $this->getDataGenerator()->create_user(); $limit = 5; $seen = 0; // The factory will skip the first $limit events. $factory = new action_event_test_factory(function($actionevent) use (&$seen, $limit) { if ($seen < $limit) { $seen++; return false; } else { return true; } }); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); for ($i = 1; $i < 21; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => 1 ]); } $events = $vault->get_action_events_by_timesort($user, 1, 20, null, $limit); $this->assertCount($limit, $events); $this->assertEquals(sprintf('Event %d', $limit + 1), $events[0]->get_name()); $this->assertEquals(sprintf('Event %d', $limit + 2), $events[1]->get_name()); $this->assertEquals(sprintf('Event %d', $limit + 3), $events[2]->get_name()); $this->assertEquals(sprintf('Event %d', $limit + 4), $events[3]->get_name()); $this->assertEquals(sprintf('Event %d', $limit + 5), $events[4]->get_name()); } /** * Test that get_action_events_by_timesort returns events between the * provided timesort values and after the last seen event when one is * provided. This should work even when the event ids aren't ordered the * same as the timesort order. */ public function test_get_action_events_by_timesort_non_consecutive_ids(): void { $this->resetAfterTest(true); $this->setAdminuser(); $user = $this->getDataGenerator()->create_user(); $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); /* * The events should be ordered by timesort as follows: * * 1 event 1 * 2 event 1 * 1 event 2 * 2 event 2 * 1 event 3 * 2 event 3 * 1 event 4 * 2 event 4 * 1 event 5 * 2 event 5 * 1 event 6 * 2 event 6 * 1 event 7 * 2 event 7 * 1 event 8 * 2 event 8 * 1 event 9 * 2 event 9 * 1 event 10 * 2 event 10 */ $records = []; for ($i = 1; $i < 11; $i++) { $records[] = create_event([ 'name' => sprintf('1 event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => 1 ]); } for ($i = 1; $i < 11; $i++) { $records[] = create_event([ 'name' => sprintf('2 event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => 1 ]); } /* * Expected result set: * * 2 event 4 * 1 event 5 * 2 event 5 * 1 event 6 * 2 event 6 * 1 event 7 * 2 event 7 * 1 event 8 * 2 event 8 */ $aftereventid = $records[3]->id; $afterevent = $vault->get_event_by_id($aftereventid); // Offset results by event with name "1 event 4" which has the same timesort // value as the lower boundary of this query (3). Confirm that the given // $afterevent is used to ignore events with the same timesortfrom values. $events = $vault->get_action_events_by_timesort($user, 3, 8, $afterevent); $this->assertCount(9, $events); $this->assertEquals('2 event 4', $events[0]->get_name()); $this->assertEquals('2 event 8', $events[8]->get_name()); /* * Expected result set: * * 2 event 4 * 1 event 5 */ $events = $vault->get_action_events_by_timesort($user, 3, 8, $afterevent, 2); $this->assertCount(2, $events); $this->assertEquals('2 event 4', $events[0]->get_name()); $this->assertEquals('1 event 5', $events[1]->get_name()); /* * Expected result set: * * 2 event 8 */ $aftereventid = $records[7]->id; $afterevent = $vault->get_event_by_id($aftereventid); // Offset results by event with name "1 event 8" which has the same timesort // value as the upper boundary of this query (8). Confirm that the given // $afterevent is used to ignore events with the same timesortto values. $events = $vault->get_action_events_by_timesort($user, 3, 8, $afterevent); $this->assertCount(1, $events); $this->assertEquals('2 event 8', $events[0]->get_name()); /* * Expected empty result set. */ $aftereventid = $records[18]->id; $afterevent = $vault->get_event_by_id($aftereventid); // Offset results by event with name "2 event 9" which has a timesort // value larger than the upper boundary of this query (9 > 8). Confirm // that the given $afterevent is used for filtering events. $events = $vault->get_action_events_by_timesort($user, 3, 8, $afterevent); $this->assertEmpty($events); } /** * There are subtle cases where the priority of an event override may be identical to another. * For example, if you duplicate a group override, but make it apply to a different group. Now * there are two overrides with exactly the same overridden dates. In this case the priority of * both is 1. * * In this situation: * - A user in group A should see only the A override * - A user in group B should see only the B override * - A user in both A and B should see both */ public function test_get_action_events_by_timesort_with_identical_group_override_priorities(): void { $this->resetAfterTest(); $this->setAdminuser(); $course = $this->getDataGenerator()->create_course(); // Create an assign instance. $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $assigninstance = $assigngenerator->create_instance(['course' => $course->id]); // Create users. $users = [ 'Only in group A' => $this->getDataGenerator()->create_user(), 'Only in group B' => $this->getDataGenerator()->create_user(), 'In group A and B' => $this->getDataGenerator()->create_user(), 'In no groups' => $this->getDataGenerator()->create_user() ]; // Enrol users. foreach ($users as $user) { $this->getDataGenerator()->enrol_user($user->id, $course->id); } // Create groups. $groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]); // Add members to groups. // Group A. $this->getDataGenerator()->create_group_member(['groupid' => $groupa->id, 'userid' => $users['Only in group A']->id]); $this->getDataGenerator()->create_group_member(['groupid' => $groupa->id, 'userid' => $users['In group A and B']->id]); // Group B. $this->getDataGenerator()->create_group_member(['groupid' => $groupb->id, 'userid' => $users['Only in group B']->id]); $this->getDataGenerator()->create_group_member(['groupid' => $groupb->id, 'userid' => $users['In group A and B']->id]); // Events with the same module name, instance and event type. $events = [ [ 'name' => 'Assignment 1 due date - Group A override', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => $groupa->id, 'userid' => 2, 'modulename' => 'assign', 'instance' => $assigninstance->id, 'eventtype' => 'due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 1, 'timeduration' => 0, 'visible' => 1, 'priority' => 1 ], [ 'name' => 'Assignment 1 due date - Group B override', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => $groupb->id, 'userid' => 2, 'modulename' => 'assign', 'instance' => $assigninstance->id, 'eventtype' => 'due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 1, 'timeduration' => 0, 'visible' => 1, 'priority' => 1 ], [ 'name' => 'Assignment 1 due date', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'assign', 'instance' => $assigninstance->id, 'eventtype' => 'due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 1, 'timeduration' => 0, 'visible' => 1, 'priority' => null, ] ]; foreach ($events as $event) { \calendar_event::create($event, false); } $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); $usersevents = array_reduce(array_keys($users), function($carry, $description) use ($users, $vault) { // NB: This is currently needed to make get_action_events_by_timesort return the right thing. // It needs to be fixed, see MDL-58736. $this->setUser($users[$description]); return $carry + ['For user ' . lcfirst($description) => $vault->get_action_events_by_timesort($users[$description])]; }, []); foreach ($usersevents as $description => $userevents) { if ($description == 'For user in group A and B') { // User is in both A and B, so they should see the override for both // given that the priority is the same. $this->assertCount(2, $userevents); continue; } // Otherwise there should be only one assign event for each user. $this->assertCount(1, $userevents); } // User in only group A should see the group A override. $this->assertEquals('Assignment 1 due date - Group A override', $usersevents['For user only in group A'][0]->get_name()); // User in only group B should see the group B override. $this->assertEquals('Assignment 1 due date - Group B override', $usersevents['For user only in group B'][0]->get_name()); // User in group A and B should see see both overrides since the priorities are the same. $this->assertEquals('Assignment 1 due date - Group A override', $usersevents['For user in group A and B'][0]->get_name()); $this->assertEquals('Assignment 1 due date - Group B override', $usersevents['For user in group A and B'][1]->get_name()); // User in no groups should see the plain assignment event. $this->assertEquals('Assignment 1 due date', $usersevents['For user in no groups'][0]->get_name()); } /** * Test that if a user is suspended that events related to that course are not shown. * User 1 is suspended. User 2 is active. */ public function test_get_action_events_by_timesort_with_suspended_user(): void { $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $this->setAdminuser(); $lesson = $this->getDataGenerator()->create_module('lesson', [ 'name' => 'Lesson 1', 'course' => $course->id, 'available' => time(), 'deadline' => (time() + (60 * 60 * 24 * 5)) ] ); $this->getDataGenerator()->enrol_user($user1->id, $course->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED); $this->getDataGenerator()->enrol_user($user2->id, $course->id); $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); $user1events = $vault->get_action_events_by_timesort($user1, null, null, null, 20, true); $this->assertEmpty($user1events); $user2events = $vault->get_action_events_by_timesort($user2, null, null, null, 20, true); $this->assertCount(1, $user2events); $this->assertEquals('Lesson 1 closes', $user2events[0]->get_name()); } /** * Test that get_action_events_by_course returns events after the * provided timesort value. */ public function test_get_action_events_by_course_after_time(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); $this->resetAfterTest(true); $this->setAdminuser(); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); for ($i = 1; $i < 6; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course1->id, ]); } for ($i = 6; $i < 12; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course2->id, ]); } $events = $vault->get_action_events_by_course($user, $course1, 3); $this->assertCount(3, $events); $this->assertEquals('Event 3', $events[0]->get_name()); $this->assertEquals('Event 4', $events[1]->get_name()); $this->assertEquals('Event 5', $events[2]->get_name()); $events = $vault->get_action_events_by_course($user, $course1, 3, null, null, 1); $this->assertCount(1, $events); $this->assertEquals('Event 3', $events[0]->get_name()); $events = $vault->get_action_events_by_course($user, $course1, 6); $this->assertCount(0, $events); } /** * Test that get_action_events_by_course returns events before the * provided timesort value. */ public function test_get_action_events_by_course_before_time(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); $this->resetAfterTest(true); $this->setAdminuser(); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); for ($i = 1; $i < 6; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course1->id, ]); } for ($i = 6; $i < 12; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course2->id, ]); } $events = $vault->get_action_events_by_course($user, $course1, null, 3); $this->assertCount(3, $events); $this->assertEquals('Event 1', $events[0]->get_name()); $this->assertEquals('Event 2', $events[1]->get_name()); $this->assertEquals('Event 3', $events[2]->get_name()); $events = $vault->get_action_events_by_course($user, $course1, null, 3, null, 1); $this->assertCount(1, $events); $this->assertEquals('Event 1', $events[0]->get_name()); $events = $vault->get_action_events_by_course($user, $course1, 6); $this->assertCount(0, $events); } /** * Test that get_action_events_by_course returns events between the * provided timesort values. */ public function test_get_action_events_by_course_between_time(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); $this->resetAfterTest(true); $this->setAdminuser(); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); for ($i = 1; $i < 6; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course1->id, ]); } for ($i = 6; $i < 12; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course2->id, ]); } $events = $vault->get_action_events_by_course($user, $course1, 2, 4); $this->assertCount(3, $events); $this->assertEquals('Event 2', $events[0]->get_name()); $this->assertEquals('Event 3', $events[1]->get_name()); $this->assertEquals('Event 4', $events[2]->get_name()); $events = $vault->get_action_events_by_course($user, $course1, 2, 4, null, 1); $this->assertCount(1, $events); $this->assertEquals('Event 2', $events[0]->get_name()); } /** * Test that get_action_events_by_course returns events between the * provided timesort values and after the last seen event when one is * provided. */ public function test_get_action_events_by_course_between_time_after_event(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); $records = []; $this->resetAfterTest(true); $this->setAdminuser(); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); for ($i = 1; $i < 21; $i++) { $records[] = create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course1->id, ]); } for ($i = 21; $i < 41; $i++) { $records[] = create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course2->id, ]); } $aftereventid = $records[6]->id; $afterevent = $vault->get_event_by_id($aftereventid); $events = $vault->get_action_events_by_course($user, $course1, 3, 15, $afterevent); $this->assertCount(8, $events); $this->assertEquals('Event 8', $events[0]->get_name()); $events = $vault->get_action_events_by_course($user, $course1, 3, 15, $afterevent, 3); $this->assertCount(3, $events); } /** * Test that get_action_events_by_course returns events between the * provided timesort values and the last seen event can be provided to * get paginated results. */ public function test_get_action_events_by_course_between_time_skip_even_records(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); // The factory will return every event that is divisible by 2. $factory = new action_event_test_factory(function($actionevent) { static $count = 0; $count++; return ($count % 2) ? true : false; }); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); $this->resetAfterTest(true); $this->setAdminuser(); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); for ($i = 1; $i < 41; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course1->id, ]); } for ($i = 41; $i < 81; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course2->id, ]); } $events = $vault->get_action_events_by_course($user, $course1, 3, 35, null, 5); $this->assertCount(5, $events); $this->assertEquals('Event 3', $events[0]->get_name()); $this->assertEquals('Event 5', $events[1]->get_name()); $this->assertEquals('Event 7', $events[2]->get_name()); $this->assertEquals('Event 9', $events[3]->get_name()); $this->assertEquals('Event 11', $events[4]->get_name()); $afterevent = $events[4]; $events = $vault->get_action_events_by_course($user, $course1, 3, 35, $afterevent, 5); $this->assertCount(5, $events); $this->assertEquals('Event 13', $events[0]->get_name()); $this->assertEquals('Event 15', $events[1]->get_name()); $this->assertEquals('Event 17', $events[2]->get_name()); $this->assertEquals('Event 19', $events[3]->get_name()); $this->assertEquals('Event 21', $events[4]->get_name()); } /** * Test that get_action_events_by_course returns events between the * provided timesort values. The database will continue to be read until the * number of events requested has been satisfied. In this case the first * five events are rejected so it should require two database requests. */ public function test_get_action_events_by_course_between_time_skip_first_records(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $limit = 5; $seen = 0; // The factory will skip the first $limit events. $factory = new action_event_test_factory(function($actionevent) use (&$seen, $limit) { if ($seen < $limit) { $seen++; return false; } else { return true; } }); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); $this->resetAfterTest(true); $this->setAdminuser(); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); for ($i = 1; $i < 21; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course1->id, ]); } for ($i = 21; $i < 41; $i++) { create_event([ 'name' => sprintf('Event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course2->id, ]); } $events = $vault->get_action_events_by_course($user, $course1, 1, 20, null, $limit); $this->assertCount($limit, $events); $this->assertEquals(sprintf('Event %d', $limit + 1), $events[0]->get_name()); $this->assertEquals(sprintf('Event %d', $limit + 2), $events[1]->get_name()); $this->assertEquals(sprintf('Event %d', $limit + 3), $events[2]->get_name()); $this->assertEquals(sprintf('Event %d', $limit + 4), $events[3]->get_name()); $this->assertEquals(sprintf('Event %d', $limit + 5), $events[4]->get_name()); } /** * Test that get_action_events_by_course returns events between the * provided timesort values and after the last seen event when one is * provided. This should work even when the event ids aren't ordered the * same as the timesort order. */ public function test_get_action_events_by_course_non_consecutive_ids(): void { $this->resetAfterTest(true); $this->setAdminuser(); $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); $this->setAdminuser(); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); /* * The events should be ordered by timesort as follows: * * 1 event 1 * 2 event 1 * 1 event 2 * 2 event 2 * 1 event 3 * 2 event 3 * 1 event 4 * 2 event 4 * 1 event 5 * 2 event 5 * 1 event 6 * 2 event 6 * 1 event 7 * 2 event 7 * 1 event 8 * 2 event 8 * 1 event 9 * 2 event 9 * 1 event 10 * 2 event 10 */ $records = []; for ($i = 1; $i < 11; $i++) { $records[] = create_event([ 'name' => sprintf('1 event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course1->id, ]); } for ($i = 1; $i < 11; $i++) { $records[] = create_event([ 'name' => sprintf('2 event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course1->id, ]); } // Create events for the other course. for ($i = 1; $i < 11; $i++) { $records[] = create_event([ 'name' => sprintf('3 event %d', $i), 'eventtype' => 'user', 'userid' => $user->id, 'timesort' => $i, 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course2->id, ]); } /* * Expected result set: * * 2 event 4 * 1 event 5 * 2 event 5 * 1 event 6 * 2 event 6 * 1 event 7 * 2 event 7 * 1 event 8 * 2 event 8 */ $aftereventid = $records[3]->id; $afterevent = $vault->get_event_by_id($aftereventid); // Offset results by event with name "1 event 4" which has the same timesort // value as the lower boundary of this query (3). Confirm that the given // $afterevent is used to ignore events with the same timesortfrom values. $events = $vault->get_action_events_by_course($user, $course1, 3, 8, $afterevent); $this->assertCount(9, $events); $this->assertEquals('2 event 4', $events[0]->get_name()); $this->assertEquals('2 event 8', $events[8]->get_name()); /* * Expected result set: * * 2 event 4 * 1 event 5 */ $events = $vault->get_action_events_by_course($user, $course1, 3, 8, $afterevent, 2); $this->assertCount(2, $events); $this->assertEquals('2 event 4', $events[0]->get_name()); $this->assertEquals('1 event 5', $events[1]->get_name()); /* * Expected result set: * * 2 event 8 */ $aftereventid = $records[7]->id; $afterevent = $vault->get_event_by_id($aftereventid); // Offset results by event with name "1 event 8" which has the same timesort // value as the upper boundary of this query (8). Confirm that the given // $afterevent is used to ignore events with the same timesortto values. $events = $vault->get_action_events_by_course($user, $course1, 3, 8, $afterevent); $this->assertCount(1, $events); $this->assertEquals('2 event 8', $events[0]->get_name()); /* * Expected empty result set. */ $aftereventid = $records[18]->id; $afterevent = $vault->get_event_by_id($aftereventid); // Offset results by event with name "2 event 9" which has a timesort // value larger than the upper boundary of this query (9 > 8). Confirm // that the given $afterevent is used for filtering events. $events = $vault->get_action_events_by_course($user, $course1, 3, 8, $afterevent); $this->assertEmpty($events); } /** * There are subtle cases where the priority of an event override may be identical to another. * For example, if you duplicate a group override, but make it apply to a different group. Now * there are two overrides with exactly the same overridden dates. In this case the priority of * both is 1. * * In this situation: * - A user in group A should see only the A override * - A user in group B should see only the B override * - A user in both A and B should see both */ public function test_get_action_events_by_course_with_identical_group_override_priorities(): void { $this->resetAfterTest(); $this->setAdminuser(); $course = $this->getDataGenerator()->create_course(); // Create an assign instance. $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $assigninstance = $assigngenerator->create_instance(['course' => $course->id]); // Create users. $users = [ 'Only in group A' => $this->getDataGenerator()->create_user(), 'Only in group B' => $this->getDataGenerator()->create_user(), 'In group A and B' => $this->getDataGenerator()->create_user(), 'In no groups' => $this->getDataGenerator()->create_user() ]; // Enrol users. foreach ($users as $user) { $this->getDataGenerator()->enrol_user($user->id, $course->id); } // Create groups. $groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]); // Add members to groups. // Group A. $this->getDataGenerator()->create_group_member(['groupid' => $groupa->id, 'userid' => $users['Only in group A']->id]); $this->getDataGenerator()->create_group_member(['groupid' => $groupa->id, 'userid' => $users['In group A and B']->id]); // Group B. $this->getDataGenerator()->create_group_member(['groupid' => $groupb->id, 'userid' => $users['Only in group B']->id]); $this->getDataGenerator()->create_group_member(['groupid' => $groupb->id, 'userid' => $users['In group A and B']->id]); // Events with the same module name, instance and event type. $events = [ [ 'name' => 'Assignment 1 due date - Group A override', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => $groupa->id, 'userid' => 2, 'modulename' => 'assign', 'instance' => $assigninstance->id, 'eventtype' => 'due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 1, 'timeduration' => 0, 'visible' => 1, 'priority' => 1 ], [ 'name' => 'Assignment 1 due date - Group B override', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => $groupb->id, 'userid' => 2, 'modulename' => 'assign', 'instance' => $assigninstance->id, 'eventtype' => 'due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 1, 'timeduration' => 0, 'visible' => 1, 'priority' => 1 ], [ 'name' => 'Assignment 1 due date', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'assign', 'instance' => $assigninstance->id, 'eventtype' => 'due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 1, 'timeduration' => 0, 'visible' => 1, 'priority' => null, ] ]; foreach ($events as $event) { \calendar_event::create($event, false); } $factory = new action_event_test_factory(); $strategy = new raw_event_retrieval_strategy(); $vault = new event_vault($factory, $strategy); $usersevents = array_reduce(array_keys($users), function($carry, $description) use ($users, $course, $vault) { // NB: This is currently needed to make get_action_events_by_timesort return the right thing. // It needs to be fixed, see MDL-58736. $this->setUser($users[$description]); return $carry + [ 'For user ' . lcfirst($description) => $vault->get_action_events_by_course($users[$description], $course) ]; }, []); foreach ($usersevents as $description => $userevents) { if ($description == 'For user in group A and B') { // User is in both A and B, so they should see the override for both // given that the priority is the same. $this->assertCount(2, $userevents); continue; } // Otherwise there should be only one assign event for each user. $this->assertCount(1, $userevents); } // User in only group A should see the group A override. $this->assertEquals('Assignment 1 due date - Group A override', $usersevents['For user only in group A'][0]->get_name()); // User in only group B should see the group B override. $this->assertEquals('Assignment 1 due date - Group B override', $usersevents['For user only in group B'][0]->get_name()); // User in group A and B should see see both overrides since the priorities are the same. $this->assertEquals('Assignment 1 due date - Group A override', $usersevents['For user in group A and B'][0]->get_name()); $this->assertEquals('Assignment 1 due date - Group B override', $usersevents['For user in group A and B'][1]->get_name()); // User in no groups should see the plain assignment event. $this->assertEquals('Assignment 1 due date', $usersevents['For user in no groups'][0]->get_name()); } } PK*\;CCtests/std_proxy_test.phpnu[. namespace core_calendar; use core_calendar\local\event\proxies\std_proxy; /** * std_proxy testcase. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class std_proxy_test extends \advanced_testcase { /** * @var \stdClass[] $objects Array of objects to proxy. */ public $objects; public function setUp(): void { parent::setUp(); $this->objects = [ 1 => (object) [ 'member1' => 'Hello', 'member2' => 1729, 'member3' => 'Something else' ], 5 => (object) [ 'member1' => 'Hej', 'member2' => 87539319, 'member3' => 'nagot annat' ] ]; } /** * Test proxying. * * @dataProvider proxy_testcases * @param int $id Object ID. * @param string $member Object member to retrieve. * @param mixed $expected Expected value of member. */ public function test_proxy($id, $member, $expected): void { $proxy = new std_proxy($id, function($id) { return $this->objects[$id]; }); $this->assertEquals($proxy->get($member), $expected); } /** * Test setting values with a base class. * * @dataProvider proxy_testcases * @param int $id Object ID. * @param string $member Object member to retrieve. * @param mixed $storedvalue Value as would be stored externally. */ public function test_base_values($id, $member, $storedvalue): void { $proxy = new std_proxy( $id, function($id) { return $this->objects[$id]; }, (object)['member1' => 'should clobber 1'] ); $expected = $member == 'member1' ? 'should clobber 1' : $storedvalue; $this->assertEquals($proxy->get($member), $expected); } /** * Test getting a non existant member. * * @dataProvider get_set_testcases * @param int $id ID of the object being proxied. */ public function test_get_invalid_member($id): void { $proxy = new std_proxy($id, function($id) { return $this->objects[$id]; }); $this->expectException('\core_calendar\local\event\exceptions\member_does_not_exist_exception'); $proxy->get('thisdoesnotexist'); } /** * Test get proxied instance. * * @dataProvider get_set_testcases * @param int $id Object ID. */ public function test_get_proxied_instance($id): void { $proxy = new std_proxy($id, function($id) { return $this->objects[$id]; }); $this->assertEquals($proxy->get_proxied_instance(), $this->objects[$id]); } /** * Test cases for proxying test. */ public static function proxy_testcases(): array { return [ 'Object 1 member 1' => [ 1, 'member1', 'Hello' ], 'Object 1 member 2' => [ 1, 'member2', 1729 ], 'Object 1 member 3' => [ 1, 'member3', 'Something else' ], 'Object 2 member 1' => [ 5, 'member1', 'Hej' ], 'Object 2 member 2' => [ 5, 'member2', 87539319 ], 'Object 3 member 3' => [ 5, 'member3', 'nagot annat' ] ]; } /** * Test cases for getting and setting tests. */ public static function get_set_testcases(): array { return [ 'Object 1' => [1], 'Object 2' => [5] ]; } } PK*\*w tests/action_test.phpnu[. namespace core_calendar; use core_calendar\local\event\value_objects\action; /** * Action testcase. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class action_test extends \advanced_testcase { /** * Test action class getters. * * @dataProvider getters_testcases * @param array $constructorparams Associative array of constructor parameters. */ public function test_getters($constructorparams): void { $action = new action( $constructorparams['name'], $constructorparams['url'], $constructorparams['item_count'], $constructorparams['actionable'] ); foreach ($constructorparams as $name => $value) { if ($name == 'actionable') { $this->assertEquals($action->is_actionable(), $value); } else { $this->assertEquals($action->{'get_' . $name}(), $value); } } } /** * Test cases for getters test. */ public static function getters_testcases(): array { return [ 'Dataset 1' => [ 'constructorparams' => [ 'name' => 'Hello', 'url' => new \moodle_url('http://example.com'), 'item_count' => 1, 'actionable' => true ] ], 'Dataset 2' => [ 'constructorparams' => [ 'name' => 'Goodbye', 'url' => new \moodle_url('http://example.com'), 'item_count' => 2, 'actionable' => false ] ] ]; } } PK*\F8֓tests/local_api_test.phpnu[. namespace core_calendar; use core_calendar\local\event\container; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/helpers.php'); /** * Class contaning unit tests for the calendar local API. * * @package core_calendar * @copyright 2017 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_calendar\local\api */ final class local_api_test extends \advanced_testcase { /** * Tests set up */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Create a feedback activity instance and a calendar event for * that instance. * * @param array $feedbackproperties Properties to set on the feedback activity * @param array $eventproperties Properties to set on the calendar event * @return array The feedback activity and the calendar event */ protected function create_feedback_activity_and_event(array $feedbackproperties = [], array $eventproperties = []) { $generator = $this->getDataGenerator(); $course = $generator->create_course(); $mapper = container::get_event_mapper(); $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $feedback = $feedbackgenerator->create_instance(array_merge( ['course' => $course->id], $feedbackproperties )); $event = create_event(array_merge( [ 'courseid' => $course->id, 'modulename' => 'feedback', 'instance' => $feedback->id ], $eventproperties )); $event = $mapper->from_legacy_event_to_event($event); return [$feedback, $event]; } /** * Requesting calendar events from a given time should return all events with a sort * time at or after the requested time. All events prior to that time should not * be return. * * If there are no events on or after the given time then an empty result set should * be returned. */ public function test_get_calendar_action_events_by_timesort_after_time(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->resetAfterTest(true); $this->setAdminUser(); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course->id, 'modulename' => 'assign', 'instance' => $moduleinstance->id, 'userid' => $user->id, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => 1, ]; $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); $this->setUser($user); $result = \core_calendar\local\api::get_action_events_by_timesort(5); $this->assertCount(4, $result); $this->assertEquals('Event 5', $result[0]->get_name()); $this->assertEquals('Event 6', $result[1]->get_name()); $this->assertEquals('Event 7', $result[2]->get_name()); $this->assertEquals('Event 8', $result[3]->get_name()); $result = \core_calendar\local\api::get_action_events_by_timesort(9); $this->assertEmpty($result); $this->setAdminUser(); $result = \core_calendar\local\api::get_action_events_by_timesort(5, null, null, 20, false, $user); $this->assertCount(4, $result); } /** * Requesting calendar events before a given time should return all events with a sort * time at or before the requested time (inclusive). All events after that time * should not be returned. * * If there are no events before the given time then an empty result set should be * returned. */ public function test_get_calendar_action_events_by_timesort_before_time(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->resetAfterTest(true); $this->setAdminUser(); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course->id, 'modulename' => 'assign', 'instance' => $moduleinstance->id, 'userid' => 1, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => 1, ]; $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 2])); $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 3])); $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 4])); $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 5])); $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 6])); $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 7])); $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 8])); $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 9])); $this->setUser($user); $result = \core_calendar\local\api::get_action_events_by_timesort(null, 5); $this->assertCount(4, $result); $this->assertEquals('Event 1', $result[0]->get_name()); $this->assertEquals('Event 2', $result[1]->get_name()); $this->assertEquals('Event 3', $result[2]->get_name()); $this->assertEquals('Event 4', $result[3]->get_name()); $result = \core_calendar\local\api::get_action_events_by_timesort(null, 1); $this->assertEmpty($result); } /** * Requesting calendar events within a given time range should return all events with * a sort time between the lower and upper time bound (inclusive). * * If there are no events in the given time range then an empty result set should be * returned. */ public function test_get_calendar_action_events_by_timesort_time_range(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->resetAfterTest(true); $this->setAdminUser(); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course->id, 'modulename' => 'assign', 'instance' => $moduleinstance->id, 'userid' => 1, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => 1, ]; $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); $this->setUser($user); $result = \core_calendar\local\api::get_action_events_by_timesort(3, 6); $this->assertCount(4, $result); $this->assertEquals('Event 3', $result[0]->get_name()); $this->assertEquals('Event 4', $result[1]->get_name()); $this->assertEquals('Event 5', $result[2]->get_name()); $this->assertEquals('Event 6', $result[3]->get_name()); $result = \core_calendar\local\api::get_action_events_by_timesort(10, 15); $this->assertEmpty($result); } /** * Requesting calendar events within a given time range and a limit and offset should return * the number of events up to the given limit value that have a sort time between the lower * and uppper time bound (inclusive) where the result set is shifted by the offset value. * * If there are no events in the given time range then an empty result set should be * returned. */ public function test_get_calendar_action_events_by_timesort_time_limit_offset(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->resetAfterTest(true); $this->setAdminUser(); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course->id, 'modulename' => 'assign', 'instance' => $moduleinstance->id, 'userid' => 1, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => 1, ]; $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); $this->setUser($user); $result = \core_calendar\local\api::get_action_events_by_timesort(2, 7, $event3->id, 2); $this->assertCount(2, $result); $this->assertEquals('Event 4', $result[0]->get_name()); $this->assertEquals('Event 5', $result[1]->get_name()); $result = \core_calendar\local\api::get_action_events_by_timesort(2, 7, $event5->id, 2); $this->assertCount(2, $result); $this->assertEquals('Event 6', $result[0]->get_name()); $this->assertEquals('Event 7', $result[1]->get_name()); $result = \core_calendar\local\api::get_action_events_by_timesort(2, 7, $event7->id, 2); $this->assertEmpty($result); } /** * Test get_calendar_action_events_by_timesort with search feature. * @covers ::get_action_events_by_timesort */ public function test_get_calendar_action_events_by_timesort_with_search(): void { // Generate data. $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance = $generator->create_instance(['course' => $course->id]); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->resetAfterTest(true); $this->setAdminUser(); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'courseid' => $course->id, 'modulename' => 'assign', 'instance' => $moduleinstance->id, 'userid' => 1, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => 1, ]; $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); $event9 = create_event(array_merge($params, ['name' => 'Assign with advanced name', 'timesort' => 9])); $this->setUser($user); // No result found for fake search. $result = \core_calendar\local\api::get_action_events_by_timesort(0, null, null, 6, false, null, 'Fake search'); $this->assertEmpty($result); // Search for event name called 'Event 1'. $result = \core_calendar\local\api::get_action_events_by_timesort(0, 10, null, 20, false, null, 'Event 1'); $this->assertCount(1, $result); $this->assertEquals('Event 1', $result[0]->get_name()); // Search for event name called 'Assign with advanced name'. $result = \core_calendar\local\api::get_action_events_by_timesort( 0, 10, null, 20, false, null, 'Assign with advanced name'); $this->assertCount(1, $result); $this->assertEquals('Assign with advanced name', $result[0]->get_name()); // Search for event name contains 'Assign advanced'. $result = \core_calendar\local\api::get_action_events_by_timesort( 0, 10, null, 20, false, null, 'Assign advanced'); $this->assertCount(1, $result); $this->assertEquals('Assign with advanced name', $result[0]->get_name()); // Search for activity type called 'assign'. $result = \core_calendar\local\api::get_action_events_by_timesort(0, 10, null, 20, false, null, 'assign'); $this->assertCount(9, $result); $this->assertEquals('Event 1', $result[0]->get_name()); $this->assertEquals('Event 2', $result[1]->get_name()); $this->assertEquals('Event 3', $result[2]->get_name()); $this->assertEquals('Event 4', $result[3]->get_name()); $this->assertEquals('Event 5', $result[4]->get_name()); $this->assertEquals('Event 6', $result[5]->get_name()); $this->assertEquals('Event 7', $result[6]->get_name()); $this->assertEquals('Event 8', $result[7]->get_name()); $this->assertEquals('Assign with advanced name', $result[8]->get_name()); } /** * Requesting calendar events from a given course and time should return all * events with a sort time at or after the requested time. All events prior * to that time should not be return. * * If there are no events on or after the given time then an empty result set should * be returned. */ public function test_get_calendar_action_events_by_course_after_time(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance1 = $generator->create_instance(['course' => $course1->id]); $moduleinstance2 = $generator->create_instance(['course' => $course2->id]); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->resetAfterTest(true); $this->setUser($user); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'modulename' => 'assign', 'instance' => $moduleinstance1->id, 'userid' => $user->id, 'courseid' => $course1->id, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => 1, ]; $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); $params['courseid'] = $course2->id; $params['instance'] = $moduleinstance2->id; $event9 = create_event(array_merge($params, ['name' => 'Event 9', 'timesort' => 1])); $event10 = create_event(array_merge($params, ['name' => 'Event 10', 'timesort' => 2])); $event11 = create_event(array_merge($params, ['name' => 'Event 11', 'timesort' => 3])); $event12 = create_event(array_merge($params, ['name' => 'Event 12', 'timesort' => 4])); $event13 = create_event(array_merge($params, ['name' => 'Event 13', 'timesort' => 5])); $event14 = create_event(array_merge($params, ['name' => 'Event 14', 'timesort' => 6])); $event15 = create_event(array_merge($params, ['name' => 'Event 15', 'timesort' => 7])); $event16 = create_event(array_merge($params, ['name' => 'Event 16', 'timesort' => 8])); $result = \core_calendar\local\api::get_action_events_by_course($course1, 5); $this->assertCount(4, $result); $this->assertEquals('Event 5', $result[0]->get_name()); $this->assertEquals('Event 6', $result[1]->get_name()); $this->assertEquals('Event 7', $result[2]->get_name()); $this->assertEquals('Event 8', $result[3]->get_name()); $result = \core_calendar\local\api::get_action_events_by_course($course1, 9); $this->assertEmpty($result); } /** * Requesting calendar events for a course and before a given time should return * all events with a sort time at or before the requested time (inclusive). All * events after that time should not be returned. * * If there are no events before the given time then an empty result set should be * returned. */ public function test_get_calendar_action_events_by_course_before_time(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance1 = $generator->create_instance(['course' => $course1->id]); $moduleinstance2 = $generator->create_instance(['course' => $course2->id]); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->resetAfterTest(true); $this->setUser($user); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'modulename' => 'assign', 'instance' => $moduleinstance1->id, 'userid' => $user->id, 'courseid' => $course1->id, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => 1, ]; $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 2])); $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 3])); $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 4])); $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 5])); $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 6])); $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 7])); $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 8])); $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 9])); $params['courseid'] = $course2->id; $params['instance'] = $moduleinstance2->id; $event9 = create_event(array_merge($params, ['name' => 'Event 9', 'timesort' => 2])); $event10 = create_event(array_merge($params, ['name' => 'Event 10', 'timesort' => 3])); $event11 = create_event(array_merge($params, ['name' => 'Event 11', 'timesort' => 4])); $event12 = create_event(array_merge($params, ['name' => 'Event 12', 'timesort' => 5])); $event13 = create_event(array_merge($params, ['name' => 'Event 13', 'timesort' => 6])); $event14 = create_event(array_merge($params, ['name' => 'Event 14', 'timesort' => 7])); $event15 = create_event(array_merge($params, ['name' => 'Event 15', 'timesort' => 8])); $event16 = create_event(array_merge($params, ['name' => 'Event 16', 'timesort' => 9])); $result = \core_calendar\local\api::get_action_events_by_course($course1, null, 5); $this->assertCount(4, $result); $this->assertEquals('Event 1', $result[0]->get_name()); $this->assertEquals('Event 2', $result[1]->get_name()); $this->assertEquals('Event 3', $result[2]->get_name()); $this->assertEquals('Event 4', $result[3]->get_name()); $result = \core_calendar\local\api::get_action_events_by_course($course1, null, 1); $this->assertEmpty($result); } /** * Requesting calendar events for a course and within a given time range should * return all events with a sort time between the lower and upper time bound * (inclusive). * * If there are no events in the given time range then an empty result set should be * returned. */ public function test_get_calendar_action_events_by_course_time_range(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance1 = $generator->create_instance(['course' => $course1->id]); $moduleinstance2 = $generator->create_instance(['course' => $course2->id]); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->resetAfterTest(true); $this->setUser($user); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'modulename' => 'assign', 'instance' => $moduleinstance1->id, 'userid' => $user->id, 'courseid' => $course1->id, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => 1, ]; $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); $params['courseid'] = $course2->id; $params['instance'] = $moduleinstance2->id; $event9 = create_event(array_merge($params, ['name' => 'Event 9', 'timesort' => 1])); $event10 = create_event(array_merge($params, ['name' => 'Event 10', 'timesort' => 2])); $event11 = create_event(array_merge($params, ['name' => 'Event 11', 'timesort' => 3])); $event12 = create_event(array_merge($params, ['name' => 'Event 12', 'timesort' => 4])); $event13 = create_event(array_merge($params, ['name' => 'Event 13', 'timesort' => 5])); $event14 = create_event(array_merge($params, ['name' => 'Event 14', 'timesort' => 6])); $event15 = create_event(array_merge($params, ['name' => 'Event 15', 'timesort' => 7])); $event16 = create_event(array_merge($params, ['name' => 'Event 16', 'timesort' => 8])); $result = \core_calendar\local\api::get_action_events_by_course($course1, 3, 6); $this->assertCount(4, $result); $this->assertEquals('Event 3', $result[0]->get_name()); $this->assertEquals('Event 4', $result[1]->get_name()); $this->assertEquals('Event 5', $result[2]->get_name()); $this->assertEquals('Event 6', $result[3]->get_name()); $result = \core_calendar\local\api::get_action_events_by_course($course1, 10, 15); $this->assertEmpty($result); } /** * Requesting calendar events for a course and within a given time range and a limit * and offset should return the number of events up to the given limit value that have * a sort time between the lower and uppper time bound (inclusive) where the result * set is shifted by the offset value. * * If there are no events in the given time range then an empty result set should be * returned. */ public function test_get_calendar_action_events_by_course_time_limit_offset(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance1 = $generator->create_instance(['course' => $course1->id]); $moduleinstance2 = $generator->create_instance(['course' => $course2->id]); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->resetAfterTest(true); $this->setUser($user); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'modulename' => 'assign', 'instance' => $moduleinstance1->id, 'userid' => $user->id, 'courseid' => $course1->id, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => 1, ]; $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); $params['courseid'] = $course2->id; $params['instance'] = $moduleinstance2->id; $event9 = create_event(array_merge($params, ['name' => 'Event 9', 'timesort' => 1])); $event10 = create_event(array_merge($params, ['name' => 'Event 10', 'timesort' => 2])); $event11 = create_event(array_merge($params, ['name' => 'Event 11', 'timesort' => 3])); $event12 = create_event(array_merge($params, ['name' => 'Event 12', 'timesort' => 4])); $event13 = create_event(array_merge($params, ['name' => 'Event 13', 'timesort' => 5])); $event14 = create_event(array_merge($params, ['name' => 'Event 14', 'timesort' => 6])); $event15 = create_event(array_merge($params, ['name' => 'Event 15', 'timesort' => 7])); $event16 = create_event(array_merge($params, ['name' => 'Event 16', 'timesort' => 8])); $result = \core_calendar\local\api::get_action_events_by_course($course1, 2, 7, $event3->id, 2); $this->assertCount(2, $result); $this->assertEquals('Event 4', $result[0]->get_name()); $this->assertEquals('Event 5', $result[1]->get_name()); $result = \core_calendar\local\api::get_action_events_by_course($course1, 2, 7, $event5->id, 2); $this->assertCount(2, $result); $this->assertEquals('Event 6', $result[0]->get_name()); $this->assertEquals('Event 7', $result[1]->get_name()); $result = \core_calendar\local\api::get_action_events_by_course($course1, 2, 7, $event7->id, 2); $this->assertEmpty($result); } /** * Test that get_action_events_by_courses will return a list of events for each * course you provided as long as the user is enrolled in the course. */ public function test_get_action_events_by_courses(): void { $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance1 = $generator->create_instance(['course' => $course1->id]); $moduleinstance2 = $generator->create_instance(['course' => $course2->id]); $moduleinstance3 = $generator->create_instance(['course' => $course3->id]); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->getDataGenerator()->enrol_user($user->id, $course3->id); $this->resetAfterTest(true); $this->setUser($user); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'modulename' => 'assign', 'instance' => $moduleinstance1->id, 'userid' => $user->id, 'courseid' => $course1->id, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => 1, ]; $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); $params['courseid'] = $course2->id; $params['instance'] = $moduleinstance2->id; $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); $params['courseid'] = $course3->id; $params['instance'] = $moduleinstance3->id; $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); $event9 = create_event(array_merge($params, ['name' => 'Event 9', 'timesort' => 9])); $result = \core_calendar\local\api::get_action_events_by_courses([], 1); $this->assertEmpty($result); $result = \core_calendar\local\api::get_action_events_by_courses([$course1], 3); $this->assertEmpty($result[$course1->id]); $result = \core_calendar\local\api::get_action_events_by_courses([$course1], 1); $this->assertCount(2, $result[$course1->id]); $this->assertEquals('Event 1', $result[$course1->id][0]->get_name()); $this->assertEquals('Event 2', $result[$course1->id][1]->get_name()); $result = \core_calendar\local\api::get_action_events_by_courses([$course1, $course2], 1); $this->assertCount(2, $result[$course1->id]); $this->assertEquals('Event 1', $result[$course1->id][0]->get_name()); $this->assertEquals('Event 2', $result[$course1->id][1]->get_name()); $this->assertCount(3, $result[$course2->id]); $this->assertEquals('Event 3', $result[$course2->id][0]->get_name()); $this->assertEquals('Event 4', $result[$course2->id][1]->get_name()); $this->assertEquals('Event 5', $result[$course2->id][2]->get_name()); $result = \core_calendar\local\api::get_action_events_by_courses([$course1, $course2], 2, 4); $this->assertCount(1, $result[$course1->id]); $this->assertEquals('Event 2', $result[$course1->id][0]->get_name()); $this->assertCount(2, $result[$course2->id]); $this->assertEquals('Event 3', $result[$course2->id][0]->get_name()); $this->assertEquals('Event 4', $result[$course2->id][1]->get_name()); $result = \core_calendar\local\api::get_action_events_by_courses([$course1, $course2, $course3], 1, null, 1); $this->assertCount(1, $result[$course1->id]); $this->assertEquals('Event 1', $result[$course1->id][0]->get_name()); $this->assertCount(1, $result[$course2->id]); $this->assertEquals('Event 3', $result[$course2->id][0]->get_name()); $this->assertCount(1, $result[$course3->id]); $this->assertEquals('Event 6', $result[$course3->id][0]->get_name()); } /** * Test get_action_events_by_courses with search feature. * @covers ::get_action_events_by_courses */ public function test_get_action_events_by_courses_with_search(): void { // Generate data. $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(['fullname' => 'Course with advanced name']); $course2 = $this->getDataGenerator()->create_course(['fullname' => 'Another name']); $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $moduleinstance1 = $generator->create_instance(['course' => $course1->id]); $moduleinstance2 = $generator->create_instance(['course' => $course2->id]); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); $this->resetAfterTest(true); $this->setUser($user); $params = [ 'type' => CALENDAR_EVENT_TYPE_ACTION, 'modulename' => 'assign', 'instance' => $moduleinstance1->id, 'userid' => $user->id, 'courseid' => $course1->id, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => 1, ]; $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); $params['courseid'] = $course2->id; $params['instance'] = $moduleinstance2->id; $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); // No result found for fake search. $result = \core_calendar\local\api::get_action_events_by_courses( [$course1, $course2], 1, null, '6', 'Fake search'); $this->assertEmpty($result[$course1->id]); $this->assertEmpty($result[$course2->id]); // Search for course name called 'Course with advanced name'. $result = \core_calendar\local\api::get_action_events_by_courses( [$course1, $course2], 1, null, '6', 'Course with advanced name'); $this->assertNotEmpty($result[$course1->id]); $this->assertEmpty($result[$course2->id]); $this->assertEquals('Event 1', $result[$course1->id][0]->get_name()); $this->assertEquals('Event 2', $result[$course1->id][1]->get_name()); // Search for course name contains 'Course advanced'. $result = \core_calendar\local\api::get_action_events_by_courses( [$course1, $course2], 1, null, '6', 'Course advanced'); $this->assertNotEmpty($result[$course1->id]); $this->assertEmpty($result[$course2->id]); $this->assertEquals('Event 1', $result[$course1->id][0]->get_name()); $this->assertEquals('Event 2', $result[$course1->id][1]->get_name()); // Search for course name contains 'name'. $result = \core_calendar\local\api::get_action_events_by_courses( [$course1, $course2], 1, null, '6', 'name'); $this->assertNotEmpty($result[$course1->id]); $this->assertNotEmpty($result[$course2->id]); $this->assertEquals('Event 1', $result[$course1->id][0]->get_name()); $this->assertEquals('Event 2', $result[$course1->id][1]->get_name()); $this->assertEquals('Event 3', $result[$course2->id][0]->get_name()); $this->assertEquals('Event 4', $result[$course2->id][1]->get_name()); $this->assertEquals('Event 5', $result[$course2->id][2]->get_name()); } /** * Test that the get_legacy_events() function only returns activity events that are enabled. */ public function test_get_legacy_events_with_disabled_module(): void { global $DB; $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $assigninstance = $assigngenerator->create_instance(['course' => $course->id]); $lessongenerator = $this->getDataGenerator()->get_plugin_generator('mod_lesson'); $lessoninstance = $lessongenerator->create_instance(['course' => $course->id]); $student = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student'); $this->setUser($student); $events = [ [ 'name' => 'Start of assignment', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'assign', 'instance' => $assigninstance->id, 'eventtype' => 'due', 'timestart' => time(), 'timeduration' => 86400, 'visible' => 1 ], [ 'name' => 'Start of lesson', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'lesson', 'instance' => $lessoninstance->id, 'eventtype' => 'end', 'timestart' => time(), 'timeduration' => 86400, 'visible' => 1 ] ]; foreach ($events as $event) { \calendar_event::create($event, false); } $timestart = time() - 60; $timeend = time() + 60; // Get all events. $events = calendar_get_legacy_events($timestart, $timeend, true, 0, true); $this->assertCount(2, $events); // Disable the lesson module. $modulerecord = $DB->get_record('modules', ['name' => 'lesson']); $modulerecord->visible = 0; $DB->update_record('modules', $modulerecord); // Check that we only return the assign event. $events = calendar_get_legacy_events($timestart, $timeend, true, 0, true); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('assign', $event->modulename); } /** * Test for \core_calendar\local\api::get_legacy_events() when there are user and group overrides. */ public function test_get_legacy_events_with_overrides(): void { $generator = $this->getDataGenerator(); $course = $generator->create_course(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); if (!isset($params['course'])) { $params['course'] = $course->id; } $instance = $plugingenerator->create_instance($params); // Create users. $useroverridestudent = $generator->create_user(); $group1student = $generator->create_user(); $group2student = $generator->create_user(); $group12student = $generator->create_user(); $nogroupstudent = $generator->create_user(); // Enrol users. $generator->enrol_user($useroverridestudent->id, $course->id, 'student'); $generator->enrol_user($group1student->id, $course->id, 'student'); $generator->enrol_user($group2student->id, $course->id, 'student'); $generator->enrol_user($group12student->id, $course->id, 'student'); $generator->enrol_user($nogroupstudent->id, $course->id, 'student'); // Create groups. $group1 = $generator->create_group(['courseid' => $course->id]); $group2 = $generator->create_group(['courseid' => $course->id]); // Add members to groups. $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group1student->id]); $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group2student->id]); $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group12student->id]); $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group12student->id]); $now = time(); // Events with the same module name, instance and event type. $events = [ [ 'name' => 'Assignment 1 due date', 'description' => '', 'location' => 'Test', 'format' => 0, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'due', 'timestart' => $now, 'timeduration' => 0, 'visible' => 1 ], [ 'name' => 'Assignment 1 due date - User override', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => 0, 'groupid' => 0, 'userid' => $useroverridestudent->id, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'due', 'timestart' => $now + 86400, 'timeduration' => 0, 'visible' => 1, 'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY ], [ 'name' => 'Assignment 1 due date - Group A override', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => $course->id, 'groupid' => $group1->id, 'userid' => 2, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'due', 'timestart' => $now + (2 * 86400), 'timeduration' => 0, 'visible' => 1, 'priority' => 1, ], [ 'name' => 'Assignment 1 due date - Group B override', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => $course->id, 'groupid' => $group2->id, 'userid' => 2, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'due', 'timestart' => $now + (3 * 86400), 'timeduration' => 0, 'visible' => 1, 'priority' => 2, ], ]; foreach ($events as $event) { \calendar_event::create($event, false); } $timestart = $now - 100; $timeend = $now + (3 * 86400); $groups = [$group1->id, $group2->id]; // Get user override events. $this->setUser($useroverridestudent); $events = calendar_get_legacy_events($timestart, $timeend, $useroverridestudent->id, $groups, $course->id); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date - User override', $event->name); // Get event for user with override but with the timestart and timeend parameters only covering the original event. $events = calendar_get_legacy_events($timestart, $now, $useroverridestudent->id, $groups, $course->id); $this->assertCount(0, $events); // Get events for user that does not belong to any group and has no user override events. $this->setUser($nogroupstudent); $events = calendar_get_legacy_events($timestart, $timeend, $nogroupstudent->id, $groups, $course->id); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date', $event->name); // Get events for user that belongs to groups A and B and has no user override events. $this->setUser($group12student); $events = calendar_get_legacy_events($timestart, $timeend, $group12student->id, $groups, $course->id); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date - Group A override', $event->name); // Get events for user that belongs to group A and has no user override events. $this->setUser($group1student); $events = calendar_get_legacy_events($timestart, $timeend, $group1student->id, $groups, $course->id); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date - Group A override', $event->name); // Add repeating events. $repeatingevents = [ [ 'name' => 'Repeating site event', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => SITEID, 'groupid' => 0, 'userid' => 2, 'repeatid' => $event->id, 'modulename' => '0', 'instance' => 0, 'eventtype' => 'site', 'timestart' => $now + 86400, 'timeduration' => 0, 'visible' => 1, ], [ 'name' => 'Repeating site event', 'description' => '', 'location' => 'Test', 'format' => 1, 'courseid' => SITEID, 'groupid' => 0, 'userid' => 2, 'repeatid' => $event->id, 'modulename' => '0', 'instance' => 0, 'eventtype' => 'site', 'timestart' => $now + (2 * 86400), 'timeduration' => 0, 'visible' => 1, ], ]; foreach ($repeatingevents as $event) { \calendar_event::create($event, false); } // Make sure repeating events are not filtered out. $events = calendar_get_legacy_events($timestart, $timeend, true, true, true); $this->assertCount(3, $events); } /** * Setting the start date on the calendar event should update the date * of the event but should leave the time of day unchanged. */ public function test_update_event_start_day_updates_date(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $roleid = $generator->create_role(); $context = \context_system::instance(); $originalstarttime = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2018-02-2T10:00:00+08:00'); $expected = new \DateTimeImmutable('2018-02-2T15:00:00+08:00'); $mapper = container::get_event_mapper(); $generator->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageownentries', CAP_ALLOW, $roleid, $context, true); $this->setUser($user); $this->resetAfterTest(true); $event = create_event([ 'name' => 'Test event', 'userid' => $user->id, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => $originalstarttime->getTimestamp(), ]); $event = $mapper->from_legacy_event_to_event($event); $newEvent = \core_calendar\local\api::update_event_start_day($event, $newstartdate); $actual = $newEvent->get_times()->get_start_time(); $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp()); } /** * A user should not be able to update the start date of the event * that they don't have the capabilities to modify. */ public function test_update_event_start_day_no_permission(): void { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $roleid = $generator->create_role(); $context = \context_system::instance(); $originalstarttime = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2018-02-2T10:00:00+08:00'); $expected = new \DateTimeImmutable('2018-02-2T15:00:00+08:00'); $mapper = container::get_event_mapper(); $generator->role_assign($roleid, $user->id, $context->id); $this->setUser($user); $this->resetAfterTest(true); $event = create_event([ 'name' => 'Test event', 'userid' => $user->id, 'eventtype' => 'user', 'repeats' => 0, 'timestart' => $originalstarttime->getTimestamp(), ]); $event = $mapper->from_legacy_event_to_event($event); assign_capability('moodle/calendar:manageownentries', CAP_PROHIBIT, $roleid, $context, true); $this->expectException('moodle_exception'); $newEvent = \core_calendar\local\api::update_event_start_day($event, $newstartdate); } /** * Updating the start day of an event with no maximum cutoff should * update the corresponding activity property. * * Note: This test uses the feedback activity because it requires * module callbacks to be in place to test. */ public function test_update_event_start_day_activity_event_no_max(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/feedback/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2018-02-2T10:00:00+08:00'); $expected = new \DateTimeImmutable('2018-02-2T15:00:00+08:00'); list($feedback, $event) = $this->create_feedback_activity_and_event( [ 'timeopen' => $timeopen->getTimestamp(), 'timeclose' => 0 ], [ 'eventtype' => FEEDBACK_EVENT_TYPE_OPEN, 'timestart' => $timeopen->getTimestamp() ] ); $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate); $actual = $newevent->get_times()->get_start_time(); $feedback = $DB->get_record('feedback', ['id' => $feedback->id]); $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp()); $this->assertEquals($expected->getTimestamp(), $feedback->timeopen); } /** * Updating the start day of an event belonging to an activity to a value * less than the maximum cutoff should update the corresponding activity * property. * * Note: This test uses the feedback activity because it requires * module callbacks to be in place to test. */ public function test_update_event_start_day_activity_event_less_than_max(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/feedback/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $timeclose = new \DateTimeImmutable('2019-01-1T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2018-02-2T10:00:00+08:00'); $expected = new \DateTimeImmutable('2018-02-2T15:00:00+08:00'); list($feedback, $event) = $this->create_feedback_activity_and_event( [ 'timeopen' => $timeopen->getTimestamp(), 'timeclose' => $timeclose->getTimestamp() ], [ 'eventtype' => FEEDBACK_EVENT_TYPE_OPEN, 'timestart' => $timeopen->getTimestamp() ] ); $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate); $actual = $newevent->get_times()->get_start_time(); $feedback = $DB->get_record('feedback', ['id' => $feedback->id]); $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp()); $this->assertEquals($expected->getTimestamp(), $feedback->timeopen); } /** * Updating the start day of an event belonging to an activity to a value * equal to the maximum cutoff should update the corresponding activity * property. * * Note: This test uses the feedback activity because it requires * module callbacks to be in place to test. */ public function test_update_event_start_day_activity_event_equal_to_max(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/feedback/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $timeclose = new \DateTimeImmutable('2018-02-2T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2018-02-2T10:00:00+08:00'); list($feedback, $event) = $this->create_feedback_activity_and_event( [ 'timeopen' => $timeopen->getTimestamp(), 'timeclose' => $timeclose->getTimestamp(), ], [ 'eventtype' => FEEDBACK_EVENT_TYPE_OPEN, 'timestart' => $timeopen->getTimestamp() ] ); $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate); $actual = $newevent->get_times()->get_start_time(); $feedback = $DB->get_record('feedback', ['id' => $feedback->id]); $this->assertEquals($timeclose->getTimestamp(), $actual->getTimestamp()); $this->assertEquals($timeclose->getTimestamp(), $feedback->timeopen); } /** * Updating the start day of an event belonging to an activity to a value * after the maximum cutoff should not update the corresponding activity * property. Instead it should throw an exception. * * Note: This test uses the feedback activity because it requires * module callbacks to be in place to test. */ public function test_update_event_start_day_activity_event_after_max(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/feedback/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $timeclose = new \DateTimeImmutable('2017-02-2T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2018-02-2T10:00:00+08:00'); list($feedback, $event) = $this->create_feedback_activity_and_event( [ 'timeopen' => $timeopen->getTimestamp(), 'timeclose' => $timeclose->getTimestamp(), ], [ 'eventtype' => FEEDBACK_EVENT_TYPE_OPEN, 'timestart' => $timeopen->getTimestamp() ] ); $this->expectException('moodle_exception'); $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate); } /** * Updating the start day of an event with no minimum cutoff should * update the corresponding activity property. * * Note: This test uses the feedback activity because it requires * module callbacks to be in place to test. */ public function test_update_event_start_day_activity_event_no_min(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/feedback/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $timeclose = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2016-02-2T10:00:00+08:00'); $expected = new \DateTimeImmutable('2016-02-2T15:00:00+08:00'); list($feedback, $event) = $this->create_feedback_activity_and_event( [ 'timeopen' => 0, 'timeclose' => $timeclose->getTimestamp() ], [ 'eventtype' => FEEDBACK_EVENT_TYPE_OPEN, 'timestart' => $timeclose->getTimestamp() ] ); $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate); $actual = $newevent->get_times()->get_start_time(); $feedback = $DB->get_record('feedback', ['id' => $feedback->id]); $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp()); $this->assertEquals($expected->getTimestamp(), $feedback->timeopen); } /** * Updating the start day of an event belonging to an activity to a value * greater than the minimum cutoff should update the corresponding activity * property. * * Note: This test uses the feedback activity because it requires * module callbacks to be in place to test. */ public function test_update_event_start_day_activity_event_greater_than_min(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/feedback/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = new \DateTimeImmutable('2016-01-1T15:00:00+08:00'); $timeclose = new \DateTimeImmutable('2019-01-1T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2018-02-2T10:00:00+08:00'); $expected = new \DateTimeImmutable('2018-02-2T15:00:00+08:00'); list($feedback, $event) = $this->create_feedback_activity_and_event( [ 'timeopen' => $timeopen->getTimestamp(), 'timeclose' => $timeclose->getTimestamp() ], [ 'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE, 'timestart' => $timeclose->getTimestamp() ] ); $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate); $actual = $newevent->get_times()->get_start_time(); $feedback = $DB->get_record('feedback', ['id' => $feedback->id]); $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp()); $this->assertEquals($expected->getTimestamp(), $feedback->timeclose); } /** * Updating the start day of an event belonging to an activity to a value * equal to the minimum cutoff should update the corresponding activity * property. * * Note: This test uses the feedback activity because it requires * module callbacks to be in place to test. */ public function test_update_event_start_day_activity_event_equal_to_min(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/feedback/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $timeclose = new \DateTimeImmutable('2018-02-2T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2017-01-1T10:00:00+08:00'); $expected = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); list($feedback, $event) = $this->create_feedback_activity_and_event( [ 'timeopen' => $timeopen->getTimestamp(), 'timeclose' => $timeclose->getTimestamp(), ], [ 'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE, 'timestart' => $timeclose->getTimestamp() ] ); $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate); $actual = $newevent->get_times()->get_start_time(); $feedback = $DB->get_record('feedback', ['id' => $feedback->id]); $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp()); $this->assertEquals($expected->getTimestamp(), $feedback->timeclose); } /** * Updating the start day of an event belonging to an activity to a value * before the minimum cutoff should not update the corresponding activity * property. Instead it should throw an exception. * * Note: This test uses the feedback activity because it requires * module callbacks to be in place to test. */ public function test_update_event_start_day_activity_event_before_min(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/feedback/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $timeclose = new \DateTimeImmutable('2017-02-2T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2016-02-2T10:00:00+08:00'); list($feedback, $event) = $this->create_feedback_activity_and_event( [ 'timeopen' => $timeopen->getTimestamp(), 'timeclose' => $timeclose->getTimestamp(), ], [ 'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE, 'timestart' => $timeclose->getTimestamp() ] ); $this->expectException('moodle_exception'); $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate); } /** * Updating the start day of an overridden event belonging to an activity * should result in an exception. This is to prevent the drag and drop * of override events. * * Note: This test uses the quiz activity because it requires * module callbacks to be in place and override event support to test. */ public function test_update_event_start_day_activity_event_override(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); require_once($CFG->dirroot . '/mod/quiz/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $mapper = container::get_event_mapper(); $timeopen = new \DateTimeImmutable('2017-01-1T15:00:00+08:00'); $newstartdate = new \DateTimeImmutable('2016-02-2T10:00:00+08:00'); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $quizgenerator = $generator->get_plugin_generator('mod_quiz'); $quiz = $quizgenerator->create_instance([ 'course' => $course->id, 'timeopen' => $timeopen->getTimestamp(), ]); $event = create_event([ 'courseid' => $course->id, 'userid' => $user->id, 'modulename' => 'quiz', 'instance' => $quiz->id, 'eventtype' => QUIZ_EVENT_TYPE_OPEN, 'timestart' => $timeopen->getTimestamp() ]); $event = $mapper->from_legacy_event_to_event($event); $record = (object) [ 'quiz' => $quiz->id, 'userid' => $user->id ]; $DB->insert_record('quiz_overrides', $record); $this->expectException('moodle_exception'); $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate); } } PK*\uy??tests/event_factory_test.phpnu[. namespace core_calendar; use core_calendar\local\event\factories\event_factory; use core_calendar\local\event\entities\event_interface; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); /** * Event factory test. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class event_factory_test extends \advanced_testcase { /** * Test event class getters. * * @dataProvider create_instance_testcases * @param \stdClass $dbrow Row from the event table. * @param callable $actioncallbackapplier Action callback applier. * @param callable $visibilitycallbackapplier Visibility callback applier. * @param callable $bailoutcheck Early bail out check function. * @param string $expectedclass Class the factory is expected to produce. * @param mixed $expectedattributevalue Expected value of the modified attribute. */ public function test_create_instance( $dbrow, callable $actioncallbackapplier, callable $visibilitycallbackapplier, callable $bailoutcheck, $expectedclass, $expectedattributevalue ): void { $this->resetAfterTest(true); $this->setAdminUser(); $event = $this->create_event(); $coursecache = []; $modulecache = []; $factory = new event_factory( $actioncallbackapplier, $visibilitycallbackapplier, $bailoutcheck, $coursecache, $modulecache ); $dbrow->id = $event->id; $instance = $factory->create_instance($dbrow); if ($expectedclass) { $this->assertInstanceOf($expectedclass, $instance); } if (is_null($expectedclass)) { $this->assertNull($instance); } if ($expectedattributevalue) { $this->assertEquals( $instance->get_description()->get_value(), $expectedattributevalue ); } } /** * Test invalid callback exception. */ public function test_invalid_action_callback(): void { $this->resetAfterTest(true); $this->setAdminUser(); $event = $this->create_event(); $coursecache = []; $modulecache = []; $factory = new event_factory( function () { return 'hello'; }, function () { return true; }, function () { return false; }, $coursecache, $modulecache ); $this->expectException('\core_calendar\local\event\exceptions\invalid_callback_exception'); $factory->create_instance( (object)[ 'id' => $event->id, 'name' => 'test', 'description' => 'Test description', 'format' => 2, 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, 'repeatid' => 0, 'modulename' => 'assign', 'instance' => 1, 'eventtype' => 'due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 123456789, 'timeduration' => 12, 'timemodified' => 123456789, 'timesort' => 123456789, 'visible' => 1, 'subscriptionid' => 1, 'location' => 'Test location', ] ); } /** * Test invalid callback exception. */ public function test_invalid_visibility_callback(): void { $this->resetAfterTest(true); $this->setAdminUser(); $event = $this->create_event(); $coursecache = []; $modulecache = []; $factory = new event_factory( function ($event) { return $event; }, function () { return 'asdf'; }, function () { return false; }, $coursecache, $modulecache ); $this->expectException('\core_calendar\local\event\exceptions\invalid_callback_exception'); $factory->create_instance( (object)[ 'id' => $event->id, 'name' => 'test', 'description' => 'Test description', 'format' => 2, 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, 'repeatid' => 0, 'modulename' => 'assign', 'instance' => 1, 'eventtype' => 'due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 123456789, 'timeduration' => 12, 'timemodified' => 123456789, 'timesort' => 123456789, 'visible' => 1, 'subscriptionid' => 1, 'location' => 'Test location', ] ); } /** * Test invalid callback exception. */ public function test_invalid_bail_callback(): void { $this->resetAfterTest(true); $this->setAdminUser(); $event = $this->create_event(); $coursecache = []; $modulecache = []; $factory = new event_factory( function ($event) { return $event; }, function () { return true; }, function () { return 'asdf'; }, $coursecache, $modulecache ); $this->expectException('\core_calendar\local\event\exceptions\invalid_callback_exception'); $factory->create_instance( (object)[ 'id' => $event->id, 'name' => 'test', 'description' => 'Test description', 'format' => 2, 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, 'repeatid' => 0, 'modulename' => 'assign', 'instance' => 1, 'eventtype' => 'due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 123456789, 'timeduration' => 12, 'timemodified' => 123456789, 'timesort' => 123456789, 'visible' => 1, 'subscriptionid' => 1, 'location' => 'Test location', ] ); } /** * Test the factory's course cache. */ public function test_course_cache(): void { $this->resetAfterTest(true); $this->setAdminUser(); $course = self::getDataGenerator()->create_course(); $event = $this->create_event(['courseid' => $course->id]); $coursecache = []; $modulecache = []; $factory = new event_factory( function ($event) { return $event; }, function () { return true; }, function () { return false; }, $coursecache, $modulecache ); $instance = $factory->create_instance( (object)[ 'id' => $event->id, 'name' => 'test', 'description' => 'Test description', 'format' => 2, 'categoryid' => 0, 'courseid' => $course->id, 'groupid' => 1, 'userid' => 1, 'repeatid' => 0, 'modulename' => 'assign', 'instance' => 1, 'eventtype' => 'due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 123456789, 'timeduration' => 12, 'timemodified' => 123456789, 'timesort' => 123456789, 'visible' => 1, 'subscriptionid' => 1, 'location' => 'Test location', ] ); $instance->get_course()->get('fullname'); $this->assertArrayHasKey($course->id, $coursecache); } /** * Test the factory's module cache. */ public function test_module_cache(): void { $this->resetAfterTest(true); $this->setAdminUser(); $course = self::getDataGenerator()->create_course(); $event = $this->create_event(['courseid' => $course->id]); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $assigninstance = $plugingenerator->create_instance(['course' => $course->id]); $coursecache = []; $modulecache = []; $factory = new event_factory( function ($event) { return $event; }, function () { return true; }, function () { return false; }, $coursecache, $modulecache ); $instance = $factory->create_instance( (object)[ 'id' => $event->id, 'name' => 'test', 'description' => 'Test description', 'format' => 2, 'categoryid' => 0, 'courseid' => 0, 'groupid' => 1, 'userid' => 1, 'repeatid' => 0, 'modulename' => 'assign', 'instance' => $assigninstance->id, 'eventtype' => 'due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 123456789, 'timeduration' => 12, 'timemodified' => 123456789, 'timesort' => 123456789, 'visible' => 1, 'subscriptionid' => 1, 'location' => 'Test location', ] ); $instance->get_course_module()->get('course'); $this->assertArrayHasKey('assign' . '_' . $assigninstance->id, $modulecache); } /** * Testcases for the create instance test. * * @return array Array of testcases. */ public static function create_instance_testcases(): array { return [ 'Sample event record with event exposed' => [ 'dbrow' => (object)[ 'name' => 'Test event', 'description' => 'Hello', 'format' => 1, 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, 'repeatid' => 0, 'modulename' => 'Test module', 'instance' => 1, 'eventtype' => 'Due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 123456789, 'timeduration' => 123456789, 'timemodified' => 123456789, 'timesort' => 123456789, 'visible' => true, 'subscriptionid' => 1, 'location' => 'Test location', ], 'actioncallbackapplier' => function(event_interface $event) { return $event; }, 'visibilitycallbackapplier' => function(event_interface $event) { return true; }, 'bailoutcheck' => function() { return false; }, 'expectedclass' => event_interface::class, 'expectedattributevalue' => 'Hello' ], 'Sample event record with event hidden' => [ 'dbrow' => (object)[ 'name' => 'Test event', 'description' => 'Hello', 'format' => 1, 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, 'repeatid' => 0, 'modulename' => 'Test module', 'instance' => 1, 'eventtype' => 'Due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 123456789, 'timeduration' => 123456789, 'timemodified' => 123456789, 'timesort' => 123456789, 'visible' => true, 'subscriptionid' => 1, 'location' => 'Test location', ], 'actioncallbackapplier' => function(event_interface $event) { return $event; }, 'visibilitycallbackapplier' => function(event_interface $event) { return false; }, 'bailoutcheck' => function() { return false; }, 'expectedclass' => null, 'expectedattributevalue' => null ], 'Sample event record with early bail' => [ 'dbrow' => (object)[ 'name' => 'Test event', 'description' => 'Hello', 'format' => 1, 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, 'repeatid' => 0, 'modulename' => 'Test module', 'instance' => 1, 'eventtype' => 'Due', 'type' => CALENDAR_EVENT_TYPE_ACTION, 'timestart' => 123456789, 'timeduration' => 123456789, 'timemodified' => 123456789, 'timesort' => 123456789, 'visible' => true, 'subscriptionid' => 1, 'location' => 'Test location', ], 'actioncallbackapplier' => function(event_interface $event) { return $event; }, 'visibilitycallbackapplier' => function(event_interface $event) { return true; }, 'bailoutcheck' => function() { return true; }, 'expectedclass' => null, 'expectedattributevalue' => null ] ]; } /** * Helper function to create calendar events using the old code. * * @param array $properties A list of calendar event properties to set * @return calendar_event */ protected function create_event($properties = []) { $record = new \stdClass(); $record->name = 'event name'; $record->eventtype = 'site'; $record->timestart = time(); $record->timeduration = 0; $record->timesort = 0; $record->type = 1; $record->courseid = 0; $record->categoryid = 0; foreach ($properties as $name => $value) { $record->$name = $value; } $event = new \calendar_event($record); return $event->create($record, false); } } PK*\ Xtests/behat/humandate.featurenu[@core @core_calendar Feature: Confirm dates are human readable In order to ensure the calendar dates are human readable As an admin I need to create calendar events in near days Background: And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | calendar_upcoming | System | 1 | my-index | content | @javascript Scenario: Create human readable events Given I log in as "admin" And the following "events" exist: | name | eventtype | timestart | timeduration | # Starts today and ends today in the future | This day one hour event | user | ##today midnight +1 seconds## | 86398 | # Starts today, ends tomorrow | This day and 1 day event | user | ##today midnight +1 seconds## | 87000 | # Starts yesterday, ends today in the future | Last day one day event | user | ##today noon -1 days## | 129598 | # Starts yesterday, ends in the past | Last day less than one day event | user | ##today noon -1 days## | 86400 | # Starts tomorrow | Next day event | user | ##today midnight +1 days## | 86400 | # Far in the future | Future event | user | ##today noon +2 days## | 86400 | When I am on homepage And I click on "This day one hour event" "link" Then I should see "Today" in the "This day one hour event" "dialogue" And I should not see "Yesterday" in the "This day one hour event" "dialogue" And I should not see "Tomorrow" in the "This day one hour event" "dialogue" And "Warning" "icon" should exist in the "This day one hour event" "dialogue" And I click on "Close" "button" in the "This day one hour event" "dialogue" And I click on "This day and 1 day event" "link" And I should see "Today" in the "This day and 1 day event" "dialogue" And I should not see "Yesterday" in the "This day and 1 day event" "dialogue" And I should see "Tomorrow" in the "This day and 1 day event" "dialogue" And "Warning" "icon" should exist in the "This day and 1 day event" "dialogue" And I click on "Close" "button" in the "This day and 1 day event" "dialogue" And I click on "Last day one day event" "link" And I should see "Today" in the "Last day one day event" "dialogue" And I should see "Yesterday" in the "Last day one day event" "dialogue" And I should not see "Tomorrow" in the "Last day one day event" "dialogue" And "Warning" "icon" should exist in the "Last day one day event" "dialogue" And I click on "Close" "button" in the "Last day one day event" "dialogue" And I click on "Next day event" "link" And I should not see "Today" in the "Next day event" "dialogue" And I should not see "Yesterday" in the "Next day event" "dialogue" And I should see "Tomorrow" in the "Next day event" "dialogue" And "Warning" "icon" should exist in the "Next day event" "dialogue" And I click on "Close" "button" in the "Next day event" "dialogue" And I click on "Future event" "link" And I should not see "Today" in the "Future event" "dialogue" And I should not see "Yesterday" in the "Future event" "dialogue" And I should not see "Tomorrow" in the "Future event" "dialogue" And "Warning" "icon" should not exist in the "Future event" "dialogue" And I click on "Close" "button" in the "Future event" "dialogue" PK*\'uO O #tests/behat/calender_filter.featurenu[@core @core_calendar @javascript Feature: Perform calendar filter actions In order to test the calendar filters with preselected options As a user I need to filter calendar events by certain criteria Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "categories" exist: | name | idnumber | category | | Year | year | | | Department C1 | department-c1 | year | And the following "courses" exist: | fullname | shortname | format | category | | Course 1 | C1 | topics | department-c1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "groups" exist: | name | course | idnumber | | Group 1 | C1 | G1 | And the following "group members" exist: | user | group | | teacher1 | G1 | And the following "events" exist: | name | eventtype | | Site event | site | And the following "events" exist: | name | eventtype | course | | C1 event | course | C1 | And the following "events" exist: | name | eventtype | category | | Dep1a event | category | department-c1 | And the following "events" exist: | name | eventtype | group | course | | Group1 event | group | G1 | C1 | Scenario: Teacher of a Course can see his events and filter them Given I log in as "teacher1" When I follow "Calendar" in the user menu Then I should see "C1 event" And I should see "Dep1a event" And I should see "Group1 event" And I click on "Hide category events" "link" And I should see "C1 event" And I should not see "Dep1a event" And I should see "Group1 event" And I click on "Hide course events" "link" And I should not see "C1 event" And I should not see "Dep1a event" And I should see "Group1 event" Scenario: Teacher of a Course can see only non filtered events from user preferences calendar_savedflt Given I log in as "teacher1" And the following "user preferences" exist: | user | preference | value | | teacher1 | calendar_persistflt | 1 | | teacher1 | calendar_savedflt | 13 | When I follow "Calendar" in the user menu Then I should not see "C1 event" And I should see "Dep1a event" And I should not see "Group1 event" Scenario: Teacher of a Course can see all events because session is used and not user preference calendar_savedflt Given I log in as "teacher1" And the following "user preferences" exist: | user | preference | value | | teacher1 | calendar_persistflt | 0 | | teacher1 | calendar_savedflt | 13 | When I follow "Calendar" in the user menu Then I should see "C1 event" And I should see "Dep1a event" And I should see "Group1 event" PK*\b** tests/behat/minicalendar.featurenu[@core @core_calendar Feature: Open calendar popup In order to view calendar information As a user I need to interact with the calendar Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And I log in as "admin" @javascript Scenario: I view calendar details of a day with multiple events Given I follow "Full calendar" And I create a calendar event: | Type of event | site | | Event title | Event 1:1 | | timestart[day] | 1 | And I create a calendar event: | Type of event | site | | Event title | Event 1:2 | | timestart[day] | 1 | When I reload the page Then I should see "Event 1:1" And I should see "Event 1:2" And I am on homepage When I hover over day "1" of this month in the mini-calendar block And I should see "Event 1:1" And I should see "Event 1:2" @javascript Scenario: I view calendar details for today Given I follow "Full calendar" When I create a calendar event: | Type of event | site | | Event title | Today's event | Then I should see "Today's event" And I am on homepage And I hover over today in the mini-calendar block And I should see "Today's event" PK*\F+!tests/behat/calendar_utf8.featurenu[@core @core_calendar Feature: Verify that the day and month names are displayed using UTF-8 In order to be able to use the calendar using different languages As a user I need to see the day and month names displayed properly Background: Given remote langimport tests are enabled Scenario Outline: View the calendar for December 2012 with correct UTF-8 Given the following "language pack" exists: | language | | And I log in as "admin" And I follow "Preferences" in the user menu And I follow "Preferred language" And I set the field "Preferred language" to "" And I press "Save changes" When I view the calendar for "12" "2022" Then I should see "" And I should see "" Examples: | lang | month | day | | en_ar | December | Sat | | es | diciembre | Sáb | | fr | décembre | Sa | | ru | декабря | Сб | PK*\'eWUWUtests/behat/calendar.featurenu[@core @core_calendar Feature: Perform basic calendar functionality In order to ensure the calendar works as expected As an admin I need to create calendar data Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "categories" exist: | name | category | idnumber | | CatA | 0 | cata | And the following "courses" exist: | fullname | shortname | format | category | startdate | | Course 1 | C1 | topics | cata | ##first day of last month## | | Course 2 | C2 | topics | cata | ##first day of last month## | | Course 3 | C3 | topics | cata | ##first day of last month## | | Course 4 | C4 | topics | cata | ##first day of last month## | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student3 | C1 | student | | teacher1 | C1 | teacher | | admin | C1 | editingteacher | And the following "groups" exist: | name | course | idnumber | | Group 1 | C1 | G1 | And the following "group members" exist: | user | group | | student1 | G1 | | teacher1 | G1 | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | calendar_month | Course | C1 | course-view-* | side-pre | | calendar_upcoming | Course | C4 | course-view-* | side-pre | @javascript Scenario: Create a site event Given I log in as "admin" And I create a calendar event with form data: | Type of event | site | | Event title | Really awesome event! | | Description | Come join this awesome event, sucka! | And I log out When I am on the "Course 1" course page logged in as student1 And I follow "Course calendar" And I should see "Really awesome event!" And I log out And I log in as "student2" And I follow "Full calendar" Then I should see "Really awesome event!" @javascript Scenario: Create a course event Given I log in as "teacher1" And I create a calendar event with form data: | Type of event | course | | Course | Course 1 | | timestart[day] | 1 | | Event title | Really awesome event! & < > " ' | | Description | Come join this awesome event, sucka! | And I log out When I am on the "Course 1" course page logged in as student1 And I follow "Course calendar" And I hover over day "1" of this month in the full calendar page And I should see "Really awesome event! & < > \" '" And I click on "Really awesome event! & < > \" '" "link" And I should see "Really awesome event! & < > \" '" in the ".modal-title" "css_element" And I should see "Course 1" in the ".modal-body" "css_element" And I click on "Close" "button" in the ".modal-content" "css_element" And I log out And I log in as "student2" And I follow "Full calendar" Then I should not see "Really awesome event! & < > \" '" @javascript Scenario: Create a group event Given I log in as "teacher1" And I follow "Full calendar" And I set the field "course" to "Course 1" And I create a calendar event: | Type of event | group | | Group | Group 1 | | Event title | Really awesome event! | | Description | Come join this awesome event | And I log out When I am on the "Course 1" course page logged in as student1 And I follow "Course calendar" Then I follow "Really awesome event!" @javascript Scenario: Create a user event Given I log in as "teacher1" And I create a calendar event with form data: | Type of event | user | | Event title | Really awesome event! | | Description | Come join this awesome event, sucka! | And I log out When I am on the "Course 1" course page logged in as student1 And I follow "Course calendar" Then I should not see "Really awesome event!" @javascript Scenario: Create an event containing URL as location Given I log in as "admin" And I create a calendar event with form data: | Type of event | site | | Event title | Important webinar | | Location | https://moodle.org | When I click on "Important webinar" "link" Then "https://moodle.org" "link" should exist in the "Important webinar" "dialogue" @javascript Scenario: Delete an event Given I log in as "teacher1" And I create a calendar event with form data: | Type of event | user | | Event title | Really awesome event! | | Description | Come join this awesome event, sucka! | And I am on "Course 1" course homepage When I follow "Course calendar" And I click on "Really awesome event!" "link" And I click on "Delete" "button" in the "Really awesome event!" "dialogue" And I click on "Delete event" "button" And I wait to be redirected Then I should not see "Really awesome event!" @javascript Scenario: Edit an event Given I log in as "teacher1" And I create a calendar event with form data: | Type of event | user | | Event title | Really awesome event! | | Description | Come join this awesome event, sucka! | | Location | Cube office | And I am on "Course 1" course homepage When I follow "Course calendar" And I click on "Really awesome event!" "link" And ".location-content" "css_element" should exist And I should see "Cube office" And I click on "Edit" "button" And I set the following fields to these values: | Event title | Mediocre event :( | | Description | Wait, this event isn't that great. | | Location | | And I press "Save" And I should see "Mediocre event" And I click on "Mediocre event :(" "link" Then I should see "Mediocre event" And ".location-content" "css_element" should not exist @javascript @editor_tiny Scenario: Edit a newly created event using TinyMCE editor Given I log in as "teacher1" And I follow "Dashboard" And I click on "New event" "button" And I set the field "Event title" to "Newly created event" When I press "Save" Then I should see "Newly created event" And I click on "Newly created event" "link" And I click on "Edit" "button" in the "Newly created event" "dialogue" And I click on "Show more..." "link" And I click on the "Link" button for the "Description" TinyMCE editor And I set the field "Text to display" to "Read more..." And I set the field "Enter a URL" to "https://moodle.org/" And I click on "Create link" "button" in the "Create link" "dialogue" And I press "Save" And I click on "Newly created event" "link" And I should see "Read more..." @javascript Scenario: Module events editing Given I log in as "teacher1" And I am on "Course 1" course homepage And the following "activities" exist: | activity | course | idnumber | name | intro | timeopen | timeclose | | choice | C1 | choice1 | Test choice | Test choice description | ##today## | ##today## | When I follow "Course calendar" Then I should see "Test choice opens" And I should see "Test choice closes" When I click on "Test choice opens" "link" Then "Delete" "button" should not exist in the "Test choice opens" "dialogue" And "Edit" "button" should not exist in the "Test choice opens" "dialogue" And I should see "Course event" When I click on "Go to activity" "link" And I wait to be redirected Then I should see "Test choice" And I am on "Course 1" course homepage And I follow "Course calendar" When I click on "Test choice closes" "link" Then "Delete" "button" should not exist in the "Test choice closes" "dialogue" And "Edit" "button" should not exist in the "Test choice closes" "dialogue" And I should see "Course event" When I click on "Go to activity" "link" And I wait to be redirected Then I should see "Test choice" @javascript Scenario: Attempt to create event without fill required fields should display validation errors Given I am on the "Course 1" course page logged in as teacher1 And I follow "Course calendar" And I click on "New event" "button" When I click on "Save" "button" Then I should see "Required" in the "Event title" "form_row" And I am on homepage And I follow "Full calendar" And I click on "New event" "button" And I set the field "Event title" to "Really awesome event!" And I set the field "Type of event" to "Course" And I click on "Save" "button" And I should see "Select a course" in the "Course" "form_row" @javascript Scenario: Default event type selection in the event form Given I log in as "teacher1" When I am viewing site calendar And I click on "New event" "button" Then the field "Type of event" matches value "User" And I click on "Close" "button" in the "New event" "dialogue" And I set the field "course" to "Course 1" When I click on "New event" "button" Then the field "Type of event" matches value "Course" @javascript Scenario: Admin can only see all courses if calendar_adminseesall setting is enabled. Given I am on the "Course 1" course page logged in as admin And I am viewing site calendar And I click on "New event" "button" And I set the field "Type of event" to "Course" When I expand the "Course" autocomplete Then "Course 1" "autocomplete_suggestions" should exist And "Course 2" "autocomplete_suggestions" should not exist And "Course 3" "autocomplete_suggestions" should not exist And I click on "Close" "button" in the "New event" "dialogue" And I am on site homepage And I navigate to "Appearance > Calendar" in site administration And I set the field "Admins see all" to "1" And I press "Save changes" And I am viewing site calendar And I click on "New event" "button" And I set the field "Type of event" to "Course" When I expand the "Course" autocomplete Then "Course 1" "autocomplete_suggestions" should exist And "Course 2" "autocomplete_suggestions" should exist And "Course 3" "autocomplete_suggestions" should exist @javascript Scenario: Students can not see event type field by default. Given I log in as "student1" And I am viewing site calendar When I click on "New event" "button" # Only "user" event type is available, so "Type of event" field should not be displayed. Then "Type of event" "select" should not exist @javascript Scenario: "Student 2" has "manageentries" capability assigned but it's not enrolled in any course. Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/calendar:manageentries | Allow | student | System | | And I log in as "student2" And I am viewing site calendar When I click on "New event" "button" # Only "user" event type is available, so "Type of event" field should not be displayed. Then "Type of event" "select" should not exist @javascript Scenario: "Student 1" has "manageentries" capability assigned and it's enrolled in a course. Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/calendar:manageentries | Allow | student | System | | And I log in as "student1" And I am viewing site calendar When I click on "New event" "button" # Student 1 is enrolled in a course and have the capability assigned. # Then, the "Type of event" select box should be visible. Then "Type of event" "select" should exist And I should see "User" in the "Type of event" "select" And I should see "Course" in the "Type of event" "select" @javascript @accessibility Scenario: The calendar page must be accessible Given I am on the "Course 1" course page logged in as student1 When I follow "Course calendar" Then the page should meet accessibility standards And the page should meet "wcag131, wcag143, wcag412" accessibility standards And the page should meet accessibility standards with "wcag131, wcag143, wcag412" extra tests @javascript Scenario: The calendar page should be responsive Given I log in as "admin" And I am viewing site calendar And I create a calendar event: | Type of event | site | | Event title | Event 1:1 | | timestart[day] | 1 | When I change viewport size to "1200x1000" Then I should see "Event 1:1" And I change viewport size to "600x1000" # We need to give the browser a couple seconds to re-render the page after the screen has been resized. And I wait "1" seconds And I should not see "Event 1:1" And I hover over day "1" of this month in the full calendar page responsive view And I should see "Event 1:1" @javascript Scenario: Admin can create and edit course events if calendar_adminseesall setting is disabled Given I log in as "admin" And the following config values are set as admin: | calendar_adminseesall | 0 | And I am on "Course 4" course homepage with editing mode on And I click on "Go to calendar..." "link" in the "Upcoming events" "block" And I click on "New event" "button" And I should see "Course" in the "Type of event" "select" And "Course 4" "autocomplete_selection" should exist And I set the field "Event title" to "Test course event" And I click on "Save" "button" And I should see "Test course event" When I click on "Edit" "link" in the "region-main" "region" Then the field "Event title" matches value "Test course event" And the field "Type of event" matches value "Course" And "Course 4" "autocomplete_selection" should exist @javascript Scenario: Changing the event type should clear previous data Given I am on the "Course 1" course page logged in as admin And I follow "Course calendar" And I press "New event" And I set the following fields to these values: | Event title | Group 1 event | | Type of event | Group | And I press "Save" And I am on "Course 1" course homepage And I follow "Course calendar" And I click on "Group 1 event" "link" And I should see "Group event" And I should see "Group 1" When I click on "Edit" "button" in the "Group 1 event" "dialogue" And I set the following fields to these values: | Event title | My own user event | | Type of event | user | And I press "Save" And I click on "My own user event" "link" Then I should see "User event" And I should not see "Group 1" And I click on "Edit" "button" in the "My own user event" "dialogue" And I set the following fields to these values: | Event title | Site event | | Type of event | site | And I press "Save" And I click on "Site event" "link" And I should see "Site event" And I click on "Edit" "button" in the "Site event" "dialogue" And I set the following fields to these values: | Event title | Course 1 event | | Type of event | course | And I expand the "Course" autocomplete And I click on "Course 1" item in the autocomplete list And I press "Save" And I click on "Course 1 event" "link" And I should see "Course event" And I click on "Edit" "button" in the "Course 1 event" "dialogue" And I set the following fields to these values: | Event title | Category event | | Type of event | category | | Category | CatA | And I press "Save" And I click on "Category event" "link" And I should see "Category event" @javascript Scenario: Changing the event type from user to anything else should work Given I log in as "teacher1" # We need this so we can see the groups. And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "role assigns" exist: | user | role | contextlevel | reference | | teacher1 | manager | Category | cata | And I am on "Course 1" course homepage And I follow "Course calendar" And I press "New event" And I set the following fields to these values: | Event title | type change test event | | Type of event | User | And I press "Save" And I am on "Course 1" course homepage And I follow "Course calendar" And I click on "type change test event" "link" And I should see "User event" When I click on "Edit" "button" And I set the following fields to these values: | Event title | type change test event | | Type of event | Course | And I expand the "Course" autocomplete And I click on "Course 1" item in the autocomplete list And I press "Save" And I click on "type change test event" "link" Then I should see "Course event" # Reset to user event And I click on "Edit" "button" in the "type change test event" "dialogue" And I set the following fields to these values: | Event title | type change test event | | Type of event | User | And I press "Save" And I click on "type change test event" "link" And I should see "User event" # Now test changing from user to group event. And I am on "Course 1" course homepage And I follow "Course calendar" And I click on "type change test event" "link" And I click on "Edit" "button" And I set the following fields to these values: | Event title | type change test event | | Type of event | Group | And I click on "#fitem_id_groupcourseid .form-autocomplete-downarrow" "css_element" And I click on "Course 1" item in the autocomplete list And I set the following fields to these values: | Group | Group 1 | And I press "Save" And I click on "type change test event" "link" And I should see "Group event" # Reset to user event And I click on "Edit" "button" in the "type change test event" "dialogue" And I set the following fields to these values: | Event title | type change test event | | Type of event | User | And I press "Save" And I click on "type change test event" "link" And I should see "User event" # Now test changing from user to course event. And I click on "Edit" "button" in the "type change test event" "dialogue" And I set the following fields to these values: | Event title | Course 1 event | | Type of event | Course | And I expand the "Course" autocomplete And I click on "Course 1" item in the autocomplete list And I press "Save" And I click on "Course 1 event" "link" And I should see "Course event" # Reset to user event And I click on "Edit" "button" in the "Course 1 event" "dialogue" And I set the following fields to these values: | Event title | type change test event | | Type of event | User | And I press "Save" And I click on "type change test event" "link" And I should see "User event" # Now test changing from user to category event. And I click on "Edit" "button" in the "type change test event" "dialogue" And I set the following fields to these values: | Event title | type change test event | | Type of event | Category | And I press "Save" And I click on "type change test event" "link" And I should see "Category event" @javascript Scenario: Group multilang tags are respected on event forms Given the "multilang" filter is "on" And the "multilang" filter applies to "content and headings" And the following "courses" exist: | fullname | shortname | format | category | | Course 5 | C5 | topics | cata | And the following "groups" exist: | name | course | idnumber | | GroupGrupo 2 | C5 | G2 | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | calendar_month | Course | C5 | course-view-* | side-pre | And I am on the "Course 5" course page logged in as admin And I follow "Course calendar" # New event form. And I press "New event" When I set the following fields to these values: | Event title | Group 2 event | | Type of event | Group | Then I should see "Group 2" And I should not see "GroupGrupo 2" And I press "Save" And I am on "Course 5" course homepage And I follow "Course calendar" # Preview event modal. And I click on "Group 2 event" "link" And I should see "Group 2" And I should not see "GroupGrupo 2" # Edit event form. And I press "Edit" And I should see "Group 2" And I should not see "GroupGrupo 2" PK*\ސ$$#tests/behat/calendar_import.featurenu[@core @core_calendar @_file_upload @javascript Feature: Import and edit calendar events In order to manipulate imported calendar events As an user I need to import calendar events then edit them. Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | Scenario: Import then edit a calendar event. Given I log in as "teacher1" And I view the calendar for "1" "2016" And I click on "Import or export calendars" "link" And "Calendar" "link" should exist in the ".breadcrumb" "css_element" And "Import or export calendars" "text" should exist in the ".breadcrumb" "css_element" And I press "Import calendar" And "Calendar" "link" should exist in the ".breadcrumb" "css_element" And "Import or export calendars" "link" should exist in the ".breadcrumb" "css_element" And "Import calendar" "text" should exist in the ".breadcrumb" "css_element" And I set the following fields to these values: | Calendar name | Test Import | | Import from | Calendar file (.ics) | | Type of event | User | And I upload "calendar/tests/fixtures/import.ics" file to "Calendar file (.ics)" filemanager And I press "Import calendar" And I should see "2 events were imported" And I view the calendar for "2" "2017" And I should see "February 2017" And I should see "Event on 2-15-2017" And I should see "Event on 2-25-2017" And I click on "Event on 2-15-2017" "link" And I should see "Some place" And I click on "Edit" "button" And I set the following fields to these values: | Event title | Event on 2-20-2017 | | Description | Event on 2-20-2017 | | timestart[day] | 20 | And I press "Save" When I view the calendar for "2" "2017" Then I should see "Event on 2-20-2017" And I should see "Event on 2-25-2017" And I should not see "Event on 2-15-2017" And I click on "Import or export calendars" "link" And I click on "Delete" "link" And I should see "Are you sure you want to delete the \"Test Import\" calendar subscription?" in the ".modal .modal-body" "css_element" And I click on "Yes" "button" in the ".modal.show" "css_element" And I view the calendar for "2" "2017" And I should not see "Event on 2-25-2017" And I should not see "Event on 2-20-2017" Scenario: Import events using different event types. Given I log in as "admin" And I view the calendar for "1" "2016" And I click on "Import or export calendars" "link" And I press "Import calendar" And I set the following fields to these values: | Calendar name | Test Import | | Import from | Calendar file (.ics) | | Type of event | User | And I upload "calendar/tests/fixtures/import.ics" file to "Calendar file (.ics)" filemanager And I press "Import calendar" And I should see "User events" And I press "Import calendar" And I set the following fields to these values: | Calendar name | Test Import | | Import from | Calendar file (.ics) | | Type of event | Category | | Category | Category 1 | And I upload "calendar/tests/fixtures/import.ics" file to "Calendar file (.ics)" filemanager And I press "Import calendar" And I should see "Category events" And I press "Import calendar" And I set the following fields to these values: | Calendar name | Test Import | | Import from | Calendar file (.ics) | | Type of event | Site | And I upload "calendar/tests/fixtures/import.ics" file to "Calendar file (.ics)" filemanager And I press "Import calendar" And I should see "Site events" Scenario: Import iCalendar file with parameter. Given I log in as "admin" And I view the calendar for "7" "2022" And I click on "Import or export calendars" "link" And I press "Import calendar" And I set the following fields to these values: | Calendar name | Test Import | | Import from | Calendar file (.ics) | | Type of event | User | And I upload "calendar/tests/fixtures/import_with_parameters.ics" file to "Calendar file (.ics)" filemanager And I press "Import calendar" When I view the calendar for "1" "7" "2022" Then I should see "First event" And I should see "Description of the first event" And I should see "Second event" And I should see "Description of the second event" PK*\#&tests/behat/calendar_lookahead.featurenu[@core @core_calendar @javascript Feature: Limit displayed upcoming events In order to filter what is displayed on the calendar As a user I need to interact with the calendar Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | calendar_month | Course | C1 | course-view-* | side-pre | | calendar_upcoming | Course | C1 | course-view-* | side-pre | And I log in as "teacher1" Scenario: I view calendar details for a future event Given I am on "Course 1" course homepage with editing mode on And I follow "Course calendar" And I click on "a.next" "css_element" And I click on "a.next" "css_element" When I create a calendar event: | Type of event | course | | Course | Course 1 | | Event title | Two months away event | And I am on "Course 1" course homepage Then I should not see "Two months away event" And I am on site homepage And I follow "Preferences" in the user menu And I follow "Calendar preferences" When I set the following fields to these values: | Upcoming events look-ahead | 3 months | And I press "Save changes" And I wait to be redirected And I am on "Course 1" course homepage Then I should see "Two months away event" PK*\*u%%#tests/behat/category_events.featurenu[@core @core_calendar Feature: Course Category Events In order to inform multiple courses of shared events As a manager I need to create catgory events Background: Given the following "users" exist: | username | firstname | lastname | email | | managera | Manager | A | managera@example.com | | managera1 | Manager | A1 | managera1@example.com | | managera2 | Manager | A2 | managera2@example.com | | teachera1i | Teacher | A1i | teachera1i@example.com | | managerb | Manager | B | managerb@example.com | | managerb1 | Manager | B1 | managerb1@example.com | | managerb2 | Manager | B2 | managerb2@example.com | | teacherb1i | Teacher | B1i | teacherb1i@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "categories" exist: | name | idnumber | category | | Year | year | | | Faculty A | faculty-a | year | | Faculty B | faculty-b | year | | Department A1 | department-a1 | faculty-a | | Department A2 | department-a2 | faculty-a | | Department B1 | department-b1 | faculty-b | | Department B2 | department-b2 | faculty-b | And the following "courses" exist: | fullname | shortname | idnumber | format | category | | Course A1i | A1i | A1i | topics | department-a1 | | Course A2i | A2i | A2i | topics | department-a2 | | Course B1i | B1i | B1i | topics | department-b1 | | Course B2i | B2i | B2i | topics | department-b2 | And the following "role assigns" exist: | user | role | contextlevel | reference | | managera | manager | Category | faculty-a | | managera1 | manager | Category | department-a1 | | managerb | manager | Category | faculty-b | | managerb1 | manager | Category | department-b1 | And the following "course enrolments" exist: | user | course | role | | teachera1i | A1i | editingteacher | | teacherb1i | B1i | editingteacher | | student1 | A1i | student | | student1 | A2i | student | | student2 | B1i | student | | student2 | B2i | student | And the following "events" exist: | name | eventtype | | Site event | site | And the following "events" exist: | name | eventtype | course | | CA1i event | course | A1i | | CA2i event | course | A2i | | CB1i event | course | B1i | | CB2i event | course | B2i | And the following "events" exist: | name | eventtype | category | | FA event | category | faculty-a | | DA1 event | category | department-a1 | | DA2 event | category | department-a1 | | FB event | category | faculty-b | | DB1 event | category | department-b1 | | DB2 event | category | department-b1 | @javascript Scenario: Manager of a Category can see all child and parent events in their category Given I log in as "managera" When I follow "Calendar" in the user menu Then I should see "FA event" And I should see "DA1 event" And I should see "DA2 event" And I should not see "FB event" And I should not see "DB1 event" And I should not see "DB2 event" And I log out Given I log in as "managerb" And I follow "Calendar" in the user menu Then I should see "FB event" And I should see "DB1 event" And I should see "DB2 event" And I should not see "FA event" And I should not see "DA1 event" And I should not see "DA2 event" @javascript Scenario: Users enrolled in a course can see all child and parent events in their category Given I log in as "student1" When I follow "Calendar" in the user menu Then I should see "FA event" And I should see "DA1 event" And I should see "DA2 event" And I should see "CA1i event" And I should see "CA2i event" And I should not see "FB event" And I should not see "DB1 event" And I should not see "DB2 event" And I should not see "CB1i event" And I should not see "CB2i event" PK*\МG!!tests/behat/export.featurenu[@core @core_calendar Feature: Export calendar events In order to be able to use my calendar events outside of Moodle As a user I need to export calendar events in iCalendar format Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And I log in as "student1" Scenario: Viewing calendar export options Given I follow "Full calendar" When I click on "Import or export calendars" "link" And "Calendar" "link" should exist in the ".breadcrumb" "css_element" And "Import or export calendars" "text" should exist in the ".breadcrumb" "css_element" And I click on "Export calendar" "button" And "Calendar" "link" should exist in the ".breadcrumb" "css_element" And "Import or export calendars" "link" should exist in the ".breadcrumb" "css_element" And "Export calendar" "text" should exist in the ".breadcrumb" "css_element" Then I should see "All events" And I should see "Events related to courses" And I should see "Events related to groups" And I should see "My personal events" @javascript Scenario: Export calendar in ics format And I follow "Full calendar" And I press "New event" And I set the following fields to these values: | Event title | My event | | id_timestart_hour | 13 | | id_timestart_minute | 00 | And I press "Save" When I click on "Import or export calendars" "link" And I click on "Export calendar" "button" And I set the field "All events" to "1" And I set the field "Recent and next 60 days" to "1" And I press "Export" And I should see "SUMMARY:My event" # We need to split the step in two because Bennu library use days with leading zero and moodle removes it. And I should see "##today##DTSTART:%Y%m##" And I should see "##today##%dT050000Z##" Scenario: Generating calendar URL for all events Given I follow "Full calendar" And I click on "Import or export calendars" "link" And I click on "Export calendar" "button" And I set the field "All events" to "1" And I set the field "Recent and next 60 days" to "1" When I click on "Get calendar URL" "button" Then the "value" attribute of "Calendar URL" "field" should contain "&preset_what=all&" Scenario: Generating calendar URL for course events Given I follow "Full calendar" And I click on "Import or export calendars" "link" And I click on "Export calendar" "button" And I set the field "Events related to courses" to "1" And I set the field "Recent and next 60 days" to "1" When I click on "Get calendar URL" "button" Then the "value" attribute of "Calendar URL" "field" should contain "&preset_what=courses&" Scenario: Generating calendar URL for group events Given I follow "Full calendar" And I click on "Import or export calendars" "link" And I click on "Export calendar" "button" And I set the field "Events related to groups" to "1" And I set the field "Recent and next 60 days" to "1" When I click on "Get calendar URL" "button" Then the "value" attribute of "Calendar URL" "field" should contain "&preset_what=groups&" Scenario: Generating calendar URL for category events Given I follow "Full calendar" And I click on "Import or export calendars" "link" And I click on "Export calendar" "button" And I set the field "Events related to categories" to "1" And I set the field "Recent and next 60 days" to "1" When I click on "Get calendar URL" "button" Then the "value" attribute of "Calendar URL" "field" should contain "&preset_what=categories&" Scenario: Generating calendar URL for user events Given I follow "Full calendar" And I click on "Import or export calendars" "link" And I click on "Export calendar" "button" And I set the field "My personal events" to "1" And I set the field "Recent and next 60 days" to "1" When I click on "Get calendar URL" "button" Then the "value" attribute of "Calendar URL" "field" should contain "&preset_what=user&" PK*\"Ň&&tests/behat/behat_calendar.phpnu[. /** * Behat calendar-related steps definitions. * * @package core_calendar * @category test * @copyright 2013 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL used, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); use Behat\Gherkin\Node\TableNode as TableNode; /** * Contains functions used by behat to test functionality. * * @package core_calendar * @category test * @copyright 2013 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_calendar extends behat_base { /** * Return the list of partial named selectors. * * @return array */ public static function get_partial_named_selectors(): array { return [ new behat_component_named_selector('mini calendar block', [".//*[@data-block='calendar_month']"]), new behat_component_named_selector('full calendar page', [".//*[@id='page-calendar-view']"]), new behat_component_named_selector('calendar day', [".//*[@data-region='day'][@data-day=%locator%]"]), new behat_component_named_selector( 'calendar day detail', [".//*[@data-region='day'][@data-day=%locator%]//a[@data-action='view-day-link']"] ), new behat_component_named_selector( 'responsive calendar day', [".//*[@data-region='day'][@data-day=%locator%]/div[contains(@class, 'hidden-desktop')]"] ), new behat_component_named_selector( 'responsive calendar day detail', [".//*[@data-region='day'][@data-day=%locator%]" . "/div[contains(@class, 'hidden-desktop')]//a[@data-action='view-day-link']"] ), ]; } /** * Create event when starting on the front page. * * @Given /^I create a calendar event with form data:$/ * @param TableNode $data */ public function i_create_a_calendar_event_with_form_data($data) { // Go to current month page. $this->execute("behat_general::click_link", get_string('fullcalendar', 'calendar')); // Create event. $this->i_create_a_calendar_event($data); } /** * Create event. * * @Given /^I create a calendar event:$/ * @param TableNode $data */ public function i_create_a_calendar_event($data) { // Get the event name. $eventname = $data->getRow(1); $eventname = $eventname[1]; $this->execute("behat_general::wait_until_the_page_is_ready"); if ($this->running_javascript()) { // Click to create new event. $this->execute("behat_general::i_click_on", array(get_string('newevent', 'calendar'), "button")); // Set form fields. $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data); // Save event. $this->execute("behat_forms::press_button", get_string('save')); } } /** * Hover over a specific day in the mini-calendar. * * @Given /^I hover over day "(?P\d+)" of this month in the mini-calendar block(?P responsive view|)$/ * @param int $day The day of the current month * @param string $responsive If not null, find the responsive version of the link. */ public function i_hover_over_day_of_this_month_in_mini_calendar_block(int $day, string $responsive = ''): void { $this->execute( "behat_general::i_hover_in_the", [ $day, empty($responsive) ? 'core_calendar > calendar day' : 'core_calendar > responsive calendar day', '', 'core_calendar > mini calendar block', ], ); } /** * Hover over a specific day in the full calendar page. * * @Given /^I hover over day "(?P\d+)" of this month in the full calendar page(?P responsive view|)$/ * @param int $day The day of the current month * @param string $responsive If not empty, use the repsonsive view. */ public function i_hover_over_day_of_this_month_in_full_calendar_page(int $day, string $responsive = ''): void { $this->execute( "behat_general::i_hover_in_the", [ $day, empty($responsive) ? 'core_calendar > calendar day' : 'core_calendar > responsive calendar day', '', 'core_calendar > full calendar page', ], ); } /** * Click on a specific day in the mini-calendar. * * @Given /^I click on day "(?P\d+)" of this month in the mini-calendar block(?P responsive view|)$/ * * @param int $day The day of the current month. * @param string $responsive If not null, find the responsive version of the link. * @param string $detail If not null, find the detail version of the link. */ public function i_click_on_day_of_this_month_in_mini_calendar_block( int $day, string $responsive = '', string $detail = '', ): void { $selectortype = 'core_calendar >'; if (!empty($responsive)) { $selectortype .= ' responsive'; } $selectortype .= ' calendar day'; if (!empty($detail)) { $selectortype .= ' detail'; } $this->execute( contextapi: "behat_general::i_click_on_in_the", params: [ $day, $selectortype, '', 'core_calendar > mini calendar block', ], ); } /** * Hover over today in the mini-calendar. * * @Given /^I hover over today in the mini-calendar block( responsive view|)$/ * * @param string $responsive If not empty, use the responsive calendar link. */ public function i_hover_over_today_in_mini_calendar_block(string $responsive = ''): void { $todaysday = date('j'); $this->i_hover_over_day_of_this_month_in_mini_calendar_block($todaysday, $responsive); } /** * Click on today in the mini-calendar. * * @Given /^I click on today in the mini-calendar block( responsive view|)( to view the detail|)$/ * * @param string $responsive If not empty, use the responsive calendar link. * @param string $detail If not empty, use the detail view calendar link. */ public function i_click_on_today_in_mini_calendar_block(string $responsive = '', string $detail = ''): void { $this->i_click_on_day_of_this_month_in_mini_calendar_block( day: date('j'), responsive: $responsive, detail: $detail, ); } /** * Navigate to a specific month in the calendar. * * @Given /^I view the calendar for "(?P\d+)" "(?P\d+)"$/ * @param int $month the month selected as a number * @param int $year the four digit year */ public function i_view_the_calendar_for($month, $year) { $this->view_the_calendar('month', 1, $month, $year); } /** * Navigate to a specific date in the calendar. * * @Given /^I view the calendar for "(?P\d+)" "(?P\d+)" "(?P\d+)"$/ * @param int $day the day selected as a number * @param int $month the month selected as a number * @param int $year the four digit year */ public function i_view_the_calendar_day_view(int $day, int $month, int $year) { $this->view_the_calendar('day', $day, $month, $year); } /** * View the correct calendar view with specific day * * @param string $type type of calendar view: month or day * @param int $day the day selected as a number * @param int $month the month selected as a number * @param int $year the four digit year */ private function view_the_calendar(string $type, int $day, int $month, int $year) { $time = make_timestamp($year, $month, $day); $this->execute('behat_general::i_visit', ['/calendar/view.php?view=' . $type . '&course=1&time=' . $time]); } /** * Navigate to site calendar. * * @Given /^I am viewing site calendar$/ * @throws coding_exception * @return void */ public function i_am_viewing_site_calendar() { $this->i_am_viewing_calendar_in_view('month'); } /** * Navigate to a specific view in the calendar. * * @Given /^I am viewing calendar in "([^"]+)" view$/ * @param string $view The calendar view ('month', 'day' and 'upcoming') to navigate to. * @return void */ public function i_am_viewing_calendar_in_view(string $view): void { if (!in_array($view, ['month', 'day', 'upcoming'])) { throw new Exception("Invalid calendar view. Allowed values are: 'month', 'day' and 'upcoming'"); } $url = new moodle_url('/calendar/view.php', ['view' => $view]); $this->execute('behat_general::i_visit', [$url]); } } PK*\ߒ1tests/action_factory_test.phpnu[. namespace core_calendar; use core_calendar\local\event\entities\action_interface; /** * Action factory testcase. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class action_factory_test extends \advanced_testcase { /** * Test action factory. */ public function test_action_factory(): void { $factory = new action_factory(); $instance = $factory->create_instance( 'test', new \moodle_url('http://example.com'), 1729, true ); $this->assertInstanceOf(action_interface::class, $instance); } } PK*\.^!!event_form.phpnu[. /** * The mform for creating and editing a calendar event * * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package calendar */ /** * Always include formslib */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } require_once($CFG->dirroot.'/lib/formslib.php'); /** * The mform class for creating and editing a calendar * * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_form extends moodleform { /** * The form definition */ function definition() { global $CFG, $USER, $OUTPUT; $mform = $this->_form; $newevent = (empty($this->_customdata->event) || empty($this->_customdata->event->id)); $repeatedevents = (!empty($this->_customdata->event->eventrepeats) && $this->_customdata->event->eventrepeats>0); $hasduration = (!empty($this->_customdata->hasduration) && $this->_customdata->hasduration); $mform->addElement('header', 'general', get_string('general')); if ($newevent) { $eventtypes = $this->_customdata->eventtypes; $options = array(); if (!empty($eventtypes->user)) { $options['user'] = get_string('user'); } if (!empty($eventtypes->groups) && is_array($eventtypes->groups)) { $options['group'] = get_string('group'); } if (!empty($eventtypes->courses)) { $options['course'] = get_string('course'); } if (!empty($eventtypes->site)) { $options['site'] = get_string('site'); } $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options); $mform->addRule('eventtype', get_string('required'), 'required'); if (!empty($eventtypes->groups) && is_array($eventtypes->groups)) { $groupoptions = array(); foreach ($eventtypes->groups as $group) { $groupoptions[$group->id] = format_string($group->name, true, array('context' => context_course::instance($group->courseid))); } $mform->addElement('select', 'groupid', get_string('typegroup', 'calendar'), $groupoptions); $mform->disabledIf('groupid', 'eventtype', 'noteq', 'group'); } } // Add some hidden fields $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->setDefault('id', 0); $mform->addElement('hidden', 'courseid'); $mform->setType('courseid', PARAM_INT); $mform->addElement('hidden', 'userid'); $mform->setType('userid', PARAM_INT); $mform->setDefault('userid', $USER->id); $mform->addElement('hidden', 'modulename'); $mform->setType('modulename', PARAM_INT); $mform->setDefault('modulename', ''); $mform->addElement('hidden', 'instance'); $mform->setType('instance', PARAM_INT); $mform->setDefault('instance', 0); $mform->addElement('hidden', 'action'); $mform->setType('action', PARAM_INT); // Normal fields $mform->addElement('text', 'name', get_string('eventname','calendar'), 'size="50"'); $mform->addRule('name', get_string('required'), 'required'); $mform->setType('name', PARAM_TEXT); $mform->addElement('editor', 'description', get_string('eventdescription','calendar'), null, $this->_customdata->event->editoroptions); $mform->setType('description', PARAM_RAW); $mform->addElement('date_time_selector', 'timestart', get_string('date')); $mform->addRule('timestart', get_string('required'), 'required'); $mform->addElement('header', 'durationdetails', get_string('eventduration', 'calendar')); $group = array(); $group[] =& $mform->createElement('radio', 'duration', null, get_string('durationnone', 'calendar'), 0); $group[] =& $mform->createElement('radio', 'duration', null, get_string('durationuntil', 'calendar'), 1); $group[] =& $mform->createElement('date_time_selector', 'timedurationuntil', ''); $group[] =& $mform->createElement('radio', 'duration', null, get_string('durationminutes', 'calendar'), 2); $group[] =& $mform->createElement('text', 'timedurationminutes', get_string('durationminutes', 'calendar'), 'maxlength="7" size="7"'); $mform->addGroup($group, 'durationgroup', '', '
          ', false); $mform->disabledIf('timedurationuntil', 'duration', 'noteq', 1); $mform->disabledIf('timedurationuntil[day]', 'duration', 'noteq', 1); $mform->disabledIf('timedurationuntil[month]', 'duration', 'noteq', 1); $mform->disabledIf('timedurationuntil[year]', 'duration', 'noteq', 1); $mform->disabledIf('timedurationuntil[hour]', 'duration', 'noteq', 1); $mform->disabledIf('timedurationuntil[minute]', 'duration', 'noteq', 1); $mform->setType('timedurationminutes', PARAM_INT); $mform->disabledIf('timedurationminutes','duration','noteq', 2); $mform->addGroupRule('durationgroup', [ 'timedurationminutes' => [[get_string('maximumchars', '', 7), 'maxlength', 7, 'server']], ]); $mform->setDefault('duration', ($hasduration)?1:0); if ($newevent) { $mform->addElement('header', 'repeatevents', get_string('repeatedevents', 'calendar')); $mform->addElement('checkbox', 'repeat', get_string('repeatevent', 'calendar'), null); $mform->addElement('text', 'repeats', get_string('repeatweeksl', 'calendar'), 'maxlength="3" size="3"'); $mform->setType('repeats', PARAM_INT); $mform->setDefault('repeats', 1); $mform->disabledIf('repeats','repeat','notchecked'); $mform->addRule('repeats', get_string('maximumchars', '', 3), 'maxlength', 3, 'server'); } else if ($repeatedevents) { $mform->addElement('hidden', 'repeatid'); $mform->setType('repeatid', PARAM_INT); $mform->addElement('header', 'repeatedevents', get_string('repeatedevents', 'calendar')); $mform->addElement('radio', 'repeateditall', null, get_string('repeateditall', 'calendar', $this->_customdata->event->eventrepeats), 1); $mform->addElement('radio', 'repeateditall', null, get_string('repeateditthis', 'calendar'), 0); $mform->setDefault('repeateditall', 1); } $this->add_action_buttons(false, get_string('savechanges')); } /** * A bit of custom validation for this form * * @param array $data An assoc array of field=>value * @param array $files An array of files * @return array */ function validation($data, $files) { global $DB, $CFG; $errors = parent::validation($data, $files); if ($data['courseid'] > 0) { if ($course = $DB->get_record('course', array('id'=>$data['courseid']))) { if ($data['timestart'] < $course->startdate) { $errors['timestart'] = get_string('errorbeforecoursestart', 'calendar'); } } else { $errors['courseid'] = get_string('invalidcourse', 'error'); } } if ($data['duration'] == 1 && $data['timestart'] > $data['timedurationuntil']) { $errors['timedurationuntil'] = get_string('invalidtimedurationuntil', 'calendar'); } else if ($data['duration'] == 2 && (trim($data['timedurationminutes']) == '' || $data['timedurationminutes'] < 1)) { $errors['timedurationminutes'] = get_string('invalidtimedurationminutes', 'calendar'); } return $errors; } } PK*\dZ'Z'deprecatedlib.phpnu[. /** * List of deprecated calendar functions. * * @package core_calendar * @copyright 2025 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); use core\url; use core_calendar\output\humandate; use core_calendar\output\humantimeperiod; /** * Return the representation day. * * @param int $tstamp Timestamp in GMT * @param int|bool $now current Unix timestamp * @param bool $usecommonwords * @return string the formatted date/time * * @deprecated since Moodle 5.0. * @todo MDL-84268 Final deprecation in Moodle 6.0. */ #[\core\attribute\deprecated( replacement: '\core_calendar\output\humandate', since: '5.0', mdl: 'MDL-83873', )] function calendar_day_representation($tstamp, $now = false, $usecommonwords = true) { static $shortformat; \core\deprecation::emit_deprecation(__FUNCTION__); if (empty($shortformat)) { $shortformat = get_string('strftimedayshort'); } if ($now === false) { $now = time(); } // To have it in one place, if a change is needed. $formal = userdate($tstamp, $shortformat); $datestamp = usergetdate($tstamp); $datenow = usergetdate($now); if ($usecommonwords == false) { // We don't want words, just a date. return $formal; } else if ($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday']) { return get_string('today', 'calendar'); } else if (($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday'] - 1 ) || ($datestamp['year'] == $datenow['year'] - 1 && $datestamp['mday'] == 31 && $datestamp['mon'] == 12 && $datenow['yday'] == 1)) { return get_string('yesterday', 'calendar'); } else if (($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday'] + 1 ) || ($datestamp['year'] == $datenow['year'] + 1 && $datenow['mday'] == 31 && $datenow['mon'] == 12 && $datestamp['yday'] == 1)) { return get_string('tomorrow', 'calendar'); } else { return $formal; } } /** * return the formatted representation time. * * @param int $time the timestamp in UTC, as obtained from the database * @return string the formatted date/time * * @deprecated since Moodle 5.0. * @todo MDL-84268 Final deprecation in Moodle 6.0. */ #[\core\attribute\deprecated( replacement: '\core_calendar\output\humandate', since: '5.0', mdl: 'MDL-83873', )] function calendar_time_representation($time) { \core\deprecation::emit_deprecation(__FUNCTION__); global $OUTPUT; $humantime = humandate::create_from_timestamp( timestamp: $time, near: null, timeonly: true ); return $OUTPUT->render($humantime); } /** * Get event format time. * * @param calendar_event $event event object * @param int $now current time in gmt * @param array $linkparams list of params for event link * @param bool $usecommonwords the words as formatted date/time. * @param int $showtime determine the show time GMT timestamp * @return string $eventtime link/string for event time * * @deprecated since Moodle 5.0. * @todo MDL-84268 Final deprecation in Moodle 6.0. */ #[\core\attribute\deprecated( replacement: '\core_calendar\output\humantimeperiod', since: '5.0', mdl: 'MDL-83873', )] function calendar_format_event_time($event, $now, $linkparams = null, $usecommonwords = true, $showtime = 0) { \core\deprecation::emit_deprecation(__FUNCTION__); global $OUTPUT; $humanperiod = humantimeperiod::create_from_timestamp( starttimestamp: $event->timestart, endtimestamp: $event->timestart + $event->timeduration, link: new url(CALENDAR_URL . 'view.php'), ); return $OUTPUT->render($humanperiod); } /** * @deprecated 3.9 */ #[\core\attribute\deprecated( replacement: 'calendar_add_event_metadata no longer used', since: '3.9', mdl: 'MDL-58866', final: true, )] function calendar_add_event_metadata() { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * Get a HTML link to a course. * * @param int|stdClass $course the course id or course object * @return string a link to the course (as HTML); empty if the course id is invalid * * @deprecated since 5.0 * @todo MDL-84268 Final deprecation in Moodle 6.0. */ #[\core\attribute\deprecated( replacement: 'calendar_get_courselink no longer used', since: '5.0', mdl: 'MDL-84617', )] function calendar_get_courselink($course) { \core\deprecation::emit_deprecation(__FUNCTION__); if (!$course) { return ''; } if (!is_object($course)) { $course = calendar_get_course_cached($coursecache, $course); } $context = \context_course::instance($course->id); $fullname = format_string($course->fullname, true, ['context' => $context]); $url = new \moodle_url('/course/view.php', ['id' => $course->id]); $link = \html_writer::link($url, $fullname); return $link; } /** * Get per-day basis events * * @param array $events list of events * @param int $month the number of the month * @param int $year the number of the year * @param array $eventsbyday event on specific day * @param array $durationbyday duration of the event in days * @param array $typesbyday event type (eg: site, course, user, or group) * @param array $courses list of courses * @return void * * @deprecated since 5.0 * @todo MDL-84268 Final deprecation in Moodle 6.0. */ #[\core\attribute\deprecated( replacement: 'calendar_events_by_day not used since 3.4', since: '5.0', mdl: 'MDL-84617', )] function calendar_events_by_day($events, $month, $year, &$eventsbyday, &$durationbyday, &$typesbyday, &$courses) { \core\deprecation::emit_deprecation(__FUNCTION__); $calendartype = \core_calendar\type_factory::get_calendar_instance(); $eventsbyday = []; $typesbyday = []; $durationbyday = []; if ($events === false) { return; } foreach ($events as $event) { $startdate = $calendartype->timestamp_to_date_array($event->timestart); if ($event->timeduration) { $enddate = $calendartype->timestamp_to_date_array($event->timestart + $event->timeduration - 1); } else { $enddate = $startdate; } // Simple arithmetic: $year * 13 + $month is a distinct integer for each distinct ($year, $month) pair. if (!($startdate['year'] * 13 + $startdate['mon'] <= $year * 13 + $month) && ($enddate['year'] * 13 + $enddate['mon'] >= $year * 13 + $month)) { continue; } $eventdaystart = intval($startdate['mday']); if ($startdate['mon'] == $month && $startdate['year'] == $year) { // Give the event to its day. $eventsbyday[$eventdaystart][] = $event->id; // Mark the day as having such an event. if ($event->courseid == SITEID && $event->groupid == 0) { $typesbyday[$eventdaystart]['startsite'] = true; // Set event class for site event. $events[$event->id]->class = 'calendar_event_site'; } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) { $typesbyday[$eventdaystart]['startcourse'] = true; // Set event class for course event. $events[$event->id]->class = 'calendar_event_course'; } else if ($event->groupid) { $typesbyday[$eventdaystart]['startgroup'] = true; // Set event class for group event. $events[$event->id]->class = 'calendar_event_group'; } else if ($event->userid) { $typesbyday[$eventdaystart]['startuser'] = true; // Set event class for user event. $events[$event->id]->class = 'calendar_event_user'; } } if ($event->timeduration == 0) { // Proceed with the next. continue; } // The event starts on $month $year or before. if ($startdate['mon'] == $month && $startdate['year'] == $year) { $lowerbound = intval($startdate['mday']); } else { $lowerbound = 0; } // Also, it ends on $month $year or later. if ($enddate['mon'] == $month && $enddate['year'] == $year) { $upperbound = intval($enddate['mday']); } else { $upperbound = calendar_days_in_month($month, $year); } // Mark all days between $lowerbound and $upperbound (inclusive) as duration. for ($i = $lowerbound + 1; $i <= $upperbound; ++$i) { $durationbyday[$i][] = $event->id; if ($event->courseid == SITEID && $event->groupid == 0) { $typesbyday[$i]['durationsite'] = true; } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) { $typesbyday[$i]['durationcourse'] = true; } else if ($event->groupid) { $typesbyday[$i]['durationgroup'] = true; } else if ($event->userid) { $typesbyday[$i]['durationuser'] = true; } } } return; } PK*\_llexternallib.phpnu[PK*\o1o1 renderer.phpnu[PK*\G9V(view.phpnu[PK*\v2 Edelete.phpnu[PK*\hBWlib.phpnu[PK*\_"" \export.phpnu[PK*\ƑH H  event.phpnu[PK*\M= = 0classes/external/event_subscription_exporter.phpnu[PK*\!!!+classes/external/day_exporter.phpnu[PK*\+)!S>>#9classes/external/month_exporter.phpnu[PK*\J>Y@@# classes/external/event_exporter.phpnu[PK*\' (6classes/external/subscription/delete.phpnu[PK*\ @''*'classes/external/calendar_day_exporter.phpnu[PK*\gm#m#1Nclasses/external/events_related_objects_cache.phpnu[PK*\~0==(rclasses/external/event_exporter_base.phpnu[PK*\GM M !԰classes/external/export/token.phpnu[PK*\% "rclasses/external/week_exporter.phpnu[PK*\1hh(Lclasses/external/event_icon_exporter.phpnu[PK*\s)w) 6 classes/external/events_grouped_by_course_exporter.phpnu[PK*\IH<<,mclasses/external/calendar_event_exporter.phpnu[PK*\^ܧtt/5classes/external/calendar_upcoming_exporter.phpnu[PK*\ì&Oclasses/external/week_day_exporter.phpnu[PK*\S "cclasses/external/date_exporter.phpnu[PK*\Xagh 0mclasses/external/events_same_course_exporter.phpnu[PK*\Odd$xclasses/external/events_exporter.phpnu[PK*\HJrr*Xclasses/external/event_action_exporter.phpnu[PK*\ٽ5,$classes/external/footer_options_exporter.phpnu[PK*\^_ _ &}classes/external/day_name_exporter.phpnu[PK*\"'͛yy2classes/privacy/provider.phpnu[PK*\< .classes/type_factory.phpnu[PK*\Ҁ t5t5);classes/local/api.phpnu[PK*\Ë  Ipclasses/local/event/strategies/raw_event_retrieval_strategy_interface.phpnu[PK*\[+PWDWD?w{classes/local/event/strategies/raw_event_retrieval_strategy.phpnu[PK*\T.m )=classes/local/event/proxies/std_proxy.phpnu[PK*\#; -classes/local/event/proxies/cm_info_proxy.phpnu[PK*\PMW/_classes/local/event/proxies/proxy_interface.phpnu[PK*\ l /classes/local/event/proxies/coursecat_proxy.phpnu[PK*\!e93737!classes/local/event/container.phpnu[PK*\Wmm,g classes/local/event/mappers/event_mapper.phpnu[PK*\x60=classes/local/event/mappers/event_mapper_interface.phpnu[PK*\CMFclasses/local/event/mappers/create_update_form_mapper_interface.phpnu[PK*\9}Mclasses/local/event/mappers/create_update_form_mapper.phpnu[PK*\V1$`classes/local/event/forms/update.phpnu[PK*\:W  'iclasses/local/event/forms/eventtype.phpnu[PK*\/1}classes/local/event/forms/managesubscriptions.phpnu[PK*\gC''$classes/local/event/forms/create.phpnu[PK*\8classes/local/event/entities/repeat_event_collection.phpnu[PK*\f(;classes/local/event/entities/event_collection_interface.phpnu[PK*\&classes/local/event/entities/event.phpnu[PK*\n  0?classes/local/event/entities/event_interface.phpnu[PK*\hdd1classes/local/event/entities/action_interface.phpnu[PK*\KK-fclasses/local/event/entities/action_event.phpnu[PK*\wҥEII7classes/local/event/entities/action_event_interface.phpnu[PK*\ ,classes/local/event/value_objects/action.phpnu[PK*\,;'classes/local/event/value_objects/description_interface.phpnu[PK*\U5L-classes/local/event/value_objects/times_interface.phpnu[PK*\  1Q5classes/local/event/value_objects/event_times.phpnu[PK*\r}  7Aclasses/local/event/value_objects/event_description.phpnu[PK*\Wczz/=Iclasses/local/event/factories/event_factory.phpnu[PK*\P\9Rclasses/local/event/factories/event_factory_interface.phpnu[PK*\.:%Xclasses/local/event/factories/action_factory_interface.phpnu[PK*\o8k^classes/local/event/factories/event_abstract_factory.phpnu[PK*\WnUU9{classes/local/event/data_access/event_vault_interface.phpnu[PK*\_`=`=/_classes/local/event/data_access/event_vault.phpnu[PK*\vBclasses/local/event/exceptions/member_does_not_exist_exception.phpnu[PK*\>\classes/local/event/exceptions/invalid_parameter_exception.phpnu[PK*\qDclasses/local/event/exceptions/limit_invalid_parameter_exception.phpnu[PK*\Iݑ=classes/local/event/exceptions/invalid_callback_exception.phpnu[PK*\zGclasses/local/event/exceptions/timesort_invalid_parameter_exception.phpnu[PK*\AJ :classes/type_base.phpnu[PK*\==,W classes/output/refreshintervalcollection.phpnu[PK*\6kz!!" classes/output/humantimeperiod.phpnu[PK*\Ó228 classes/output/humandate.phpnu[PK*\bxk classes/rrule_manager.phpnu[PK*\h1 classes/action_factory.phpnu[PK*\$$r7 classes/export_form.phpnu[PK*\5!%%!H templates/month_detailed.mustachenu[PK*\D:}}&o templates/event_summary_modal.mustachenu[PK*\L u templates/calendar_mini.mustachenu[PK*\A--6| templates/event_filter.mustachenu[PK*\  templates/upcoming_mini.mustachenu[PK*\! templates/footer_options.mustachenu[PK*\WúCC# templates/add_event_button.mustachenu[PK*\! templates/day_navigation.mustachenu[PK*\))$ templates/calendar_upcoming.mustachenu[PK*\d%A& templates/event_icon.mustachenu[PK*\,%d templates/event_list.mustachenu[PK*\  templates/event_details.mustachenu[PK*\ templates/calendar_day.mustachenu[PK*\ܤ # templates/month_navigation.mustachenu[PK*\5țv templates/day_detailed.mustachenu[PK*\8TCC-` templates/subscription_update_result.mustachenu[PK*\p templates/humandate.mustachenu[PK*\+d!6 templates/month_mini.mustachenu[PK*\"7 templates/event_item.mustachenu[PK*\}*! templates/calendar_month.mustachenu[PK*\>ԝ$! templates/upcoming_detailed.mustachenu[PK*\ e (' templates/minicalendar_day_link.mustachenu[PK*\&qq#1 templates/event_filter_key.mustachenu[PK*\?L%8 templates/event_delete_modal.mustachenu[PK*\%33)? templates/calendar_upcoming_mini.mustachenu[PK*\> TE templates/header.mustachenu[PK*\M"&J templates/export_calendar_url.mustachenu[PK*\1Njj#R templates/modal_event_form.mustachenu[PK*\Pm m KY templates/view_selector.mustachenu[PK*\.%d templates/event_summary_body.mustachenu[PK*\&G"Uj templates/humantimeperiod.mustachenu[PK*\Y%]r templates/event_subscription.mustachenu[PK*\u ADD tx upgrade.txtnu[PK*\255 export_execute.phpnu[PK*\tt  index.phpnu[PK*\VQ:dd1a type/gregorian/lang/en/calendartype_gregorian.phpnu[PK*\l& type/gregorian/version.phpnu[PK*\P+ type/gregorian/classes/privacy/provider.phpnu[PK*\񒁫44$ type/gregorian/classes/structure.phpnu[PK*\l5t88' type/gregorian/tests/structure_test.phpnu[PK*\Eޒ0 % import.phpnu[PK*\9; managesubscriptions.phpnu[PK*\D>z4z4R amd/src/modal_event_form.jsnu[PK*\E֬Z#Z# amd/src/crud.jsnu[PK*\[Z,4,4z amd/src/month_view_drag_drop.jsnu[PK*\d-u amd/src/events.jsnu[PK*\IcT = amd/src/modal_delete.jsnu[PK*\P߸11f amd/src/calendar.jsnu[PK*\;{# amd/src/calendar_filter.jsnu[PK*\ΦU2 amd/src/calendar_mini.jsnu[PK*\vr CB amd/src/event_form.jsnu[PK*\ ۵O amd/src/summary_modal.jsnu[PK*\ɾ$%%%g amd/src/month_navigation_drag_drop.jsnu[PK*\2YY amd/src/view_manager.jsnu[PK*\X`n amd/src/drag_drop_data_store.jsnu[PK*\sRvv amd/src/repository.jsnu[PK*\]>sse amd/src/export.jsnu[PK*\r5WWamd/src/calendar_view.jsnu[PK*\QC   amd/src/selectors.jsnu[PK*\`11-amd/src/manage_subscriptions.jsnu[PK*\́@amd/src/popover.jsnu[PK*\$L, , Pamd/build/repository.min.jsnu[PK*\ )\amd/build/drag_drop_data_store.min.js.mapnu[PK*\'')wamd/build/manage_subscriptions.min.js.mapnu[PK*\L%% amd/build/calendar_view.min.jsnu[PK*\pp|amd/build/export.min.jsnu[PK*\]RSS%3amd/build/month_view_drag_drop.min.jsnu[PK*\B,BB۱amd/build/crud.min.jsnu[PK*\~  bamd/build/popover.min.jsnu[PK*\/dW"amd/build/calendar_view.min.js.mapnu[PK*\3X66amd/build/repository.min.js.mapnu[PK*\0TgDgD%3amd/build/modal_event_form.min.js.mapnu[PK*\eIamd/build/events.min.js.mapnu[PK*\UF>>&Ramd/build/view_manager.min.jsnu[PK*\! amd/build/modal_delete.min.js.mapnu[PK*\ ;;amd/build/selectors.min.js.mapnu[PK*\TvDDamd/build/event_form.min.js.mapnu[PK*\#4ɲamd/build/summary_modal.min.jsnu[PK*\amd/build/calendar.min.jsnu[PK*\q[[amd/build/event_form.min.jsnu[PK*\ÜStt$amd/build/calendar_filter.min.js.mapnu[PK*\F?ݢV amd/build/events.min.jsnu[PK*\Кi33 amd/build/selectors.min.jsnu[PK*\3c +amd/build/month_navigation_drag_drop.min.jsnu[PK*\C%2amd/build/drag_drop_data_store.min.jsnu[PK*\Ҡ33!%amd/build/crud.min.js.mapnu[PK*\“ɵ!Xamd/build/view_manager.min.js.mapnu[PK*\$e&%kamd/build/manage_subscriptions.min.jsnu[PK*\9 Aaamd/build/calendar_mini.min.jsnu[PK*\QЈfFfFdamd/build/calendar.min.js.mapnu[PK*\~5:amd/build/popover.min.js.mapnu[PK*\$#E#E)Qamd/build/month_view_drag_drop.min.js.mapnu[PK*\бhamd/build/export.min.js.mapnu[PK*\[  damd/build/modal_delete.min.jsnu[PK*\cnZNN amd/build/calendar_filter.min.jsnu[PK*\0:EE"]amd/build/calendar_mini.min.js.mapnu[PK*\"amd/build/summary_modal.min.js.mapnu[PK*\#!amd/build/modal_event_form.min.jsnu[PK*\$$/$amd/build/month_navigation_drag_drop.min.js.mapnu[PK*\q  %UPGRADING.mdnu[PK*\Wܡ&+tests/rrule_manager_test.phpnu[PK*\-wT!T!tests/helpers.phpnu[PK*\G^^tests/fixtures/import.icsnu[PK*\}ww)O tests/fixtures/import_with_parameters.icsnu[PK*\ #tests/event_description_test.phpnu[PK*\~+tests/event_test.phpnu[PK*\ HH?tests/privacy/provider_test.phpnu[PK*\m^^Ntests/container_test.phpnu[PK*\GT`J J (Btests/renderer_test.phpnu[PK*\n]Mtests/lib_test.phpnu[PK*\-&tests/repeat_event_collection_test.phpnu[PK*\6Y#7tests/calendar_information_test.phpnu[PK*\!X tests/event_times_test.phpnu[PK*\}tests/action_event_test.phpnu[PK*\ 4``tests/coursecat_proxy_test.phpnu[PK*\Eܡ/%/%#'tests/calendartype_test_example.phpnu[PK*\:44.Mtests/calendartype_test.phpnu[PK*\ BBB+Htests/raw_event_retrieval_strategy_test.phpnu[PK*\T %htests/output/humantimeperiod_test.phpnu[PK*\c2#2#tests/output/humandate_test.phpnu[PK*\,+tests/events_related_objects_cache_test.phpnu[PK*\554tests/externallib_test.phpnu[PK*\G"**&tJtests/calendar_event_exporter_test.phpnu[PK*\#Yk?~~utests/event/events_test.phpnu[PK*\K{pptests/cm_info_proxy_test.phpnu[PK*\F%44dtests/event_mapper_test.phpnu[PK*\$l0tests/event_vault_test.phpnu[PK*\;CCtests/std_proxy_test.phpnu[PK*\*w !tests/action_test.phpnu[PK*\F8֓tests/local_api_test.phpnu[PK*\uy?? tests/event_factory_test.phpnu[PK*\ XLtests/behat/humandate.featurenu[PK*\'uO O #[tests/behat/calender_filter.featurenu[PK*\b** Yhtests/behat/minicalendar.featurenu[PK*\F+!ntests/behat/calendar_utf8.featurenu[PK*\'eWUWUrtests/behat/calendar.featurenu[PK*\ސ$$#{tests/behat/calendar_import.featurenu[PK*\#&tests/behat/calendar_lookahead.featurenu[PK*\*u%%#tests/behat/category_events.featurenu[PK*\МG!!\tests/behat/export.featurenu[PK*\"Ň&&tests/behat/behat_calendar.phpnu[PK*\ߒ1/tests/action_factory_test.phpnu[PK*\.^!!4event_form.phpnu[PK*\dZ'Z'Vdeprecatedlib.phpnu[PK7T~