ÿØÿà 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$9\INcWWexternallib.phpnu[. /** * External course API * * @package core_course * @category external * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; use core_course\external\course_summary_exporter; use core_external\external_api; use core_external\external_description; use core_external\external_files; 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; require_once(__DIR__ . "/lib.php"); /** * Course external functions * * @package core_course * @category external * @copyright 2011 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.2 */ class core_course_external extends external_api { /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.9 Options available * @since Moodle 2.2 */ public static function get_course_contents_parameters() { return new external_function_parameters( array('courseid' => new external_value(PARAM_INT, 'course id'), 'options' => new external_multiple_structure ( new external_single_structure( array( 'name' => new external_value(PARAM_ALPHANUM, 'The expected keys (value format) are: excludemodules (bool) Do not return modules, return only the sections structure excludecontents (bool) Do not return module contents (i.e: files inside a resource) includestealthmodules (bool) Return stealth modules for students in a special section (with id -1) sectionid (int) Return only this section sectionnumber (int) Return only this section with number (order) cmid (int) Return only this module information (among the whole sections structure) modname (string) Return only modules with this name "label, forum, etc..." modid (int) Return only the module with this id (to be used with modname'), 'value' => new external_value(PARAM_RAW, 'the value of the option, this param is personaly validated in the external function.') ) ), 'Options, used since Moodle 2.9', VALUE_DEFAULT, array()) ) ); } /** * Get course contents * * @param int $courseid course id * @param array $options Options for filtering the results, used since Moodle 2.9 * @return array * @since Moodle 2.9 Options available * @since Moodle 2.2 */ public static function get_course_contents($courseid, $options = array()) { global $CFG, $DB, $USER, $PAGE; require_once($CFG->dirroot . "/course/lib.php"); require_once($CFG->libdir . '/completionlib.php'); //validate parameter $params = self::validate_parameters(self::get_course_contents_parameters(), array('courseid' => $courseid, 'options' => $options)); $filters = array(); if (!empty($params['options'])) { foreach ($params['options'] as $option) { $name = trim($option['name']); // Avoid duplicated options. if (!isset($filters[$name])) { switch ($name) { case 'excludemodules': case 'excludecontents': case 'includestealthmodules': $value = clean_param($option['value'], PARAM_BOOL); $filters[$name] = $value; break; case 'sectionid': case 'sectionnumber': case 'cmid': case 'modid': $value = clean_param($option['value'], PARAM_INT); if (is_numeric($value)) { $filters[$name] = $value; } else { throw new moodle_exception('errorinvalidparam', 'webservice', '', $name); } break; case 'modname': $value = clean_param($option['value'], PARAM_PLUGIN); if ($value) { $filters[$name] = $value; } else { throw new moodle_exception('errorinvalidparam', 'webservice', '', $name); } break; default: throw new moodle_exception('errorinvalidparam', 'webservice', '', $name); } } } } //retrieve the course $course = $DB->get_record('course', array('id' => $params['courseid']), '*', MUST_EXIST); // now security checks $context = context_course::instance($course->id, IGNORE_MISSING); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $course->id; throw new moodle_exception('errorcoursecontextnotvalid', 'webservice', '', $exceptionparam); } $canupdatecourse = has_capability('moodle/course:update', $context); //create return value $coursecontents = array(); if ($canupdatecourse or $course->visible or has_capability('moodle/course:viewhiddencourses', $context)) { //retrieve sections $modinfo = get_fast_modinfo($course); $sections = $modinfo->get_section_info_all(); $courseformat = course_get_format($course); $coursenumsections = $courseformat->get_last_section_number(); $stealthmodules = array(); // Array to keep all the modules available but not visible in a course section/topic. $completioninfo = new completion_info($course); //for each sections (first displayed to last displayed) $modinfosections = $modinfo->get_sections(); foreach ($sections as $key => $section) { // This becomes true when we are filtering and we found the value to filter with. $sectionfound = false; // Filter by section id. if (!empty($filters['sectionid'])) { if ($section->id != $filters['sectionid']) { continue; } else { $sectionfound = true; } } // Filter by section number. Note that 0 is a valid section number. if (isset($filters['sectionnumber'])) { if ($key != $filters['sectionnumber']) { continue; } else { $sectionfound = true; } } // reset $sectioncontents $sectionvalues = array(); $sectionvalues['id'] = $section->id; $sectionvalues['name'] = get_section_name($course, $section); // Temporary hack to be able to hide the subsections in certain app versions. if (!empty($section->component) && \core_useragent::is_moodle_app()) { $sectionvalues['name'] = html_writer::span($sectionvalues['name'], 'course-' . $section->component); } $sectionvalues['visible'] = $section->visible; $options = (object) array('noclean' => true); list($sectionvalues['summary'], $sectionvalues['summaryformat']) = \core_external\util::format_text($section->summary, $section->summaryformat, $context, 'course', 'section', $section->id, $options); $sectionvalues['section'] = $section->section; $sectionvalues['hiddenbynumsections'] = $section->section > $coursenumsections ? 1 : 0; $sectionvalues['uservisible'] = $section->uservisible; if (!empty($section->availableinfo)) { $sectionvalues['availabilityinfo'] = \core_availability\info::format_info($section->availableinfo, $course); } $sectionvalues['component'] = $section->component; $sectionvalues['itemid'] = $section->itemid; $sectioncontents = array(); // For each module of the section. if (empty($filters['excludemodules']) and !empty($modinfosections[$section->section])) { foreach ($modinfosections[$section->section] as $cmid) { $cm = $modinfo->cms[$cmid]; $cminfo = cm_info::create($cm); $activitydates = \core\activity_dates::get_dates_for_module($cminfo, $USER->id); // Stop here if the module is not visible to the user on the course main page: // The user can't access the module and the user can't view the module on the course page. if (!$cm->uservisible && !$cm->is_visible_on_course_page()) { continue; } // This becomes true when we are filtering and we found the value to filter with. $modfound = false; // Filter by cmid. if (!empty($filters['cmid'])) { if ($cmid != $filters['cmid']) { continue; } else { $modfound = true; } } // Filter by module name and id. if (!empty($filters['modname'])) { if ($cm->modname != $filters['modname']) { continue; } else if (!empty($filters['modid'])) { if ($cm->instance != $filters['modid']) { continue; } else { // Note that if we are only filtering by modname we don't break the loop. $modfound = true; } } } $module = array(); $modcontext = context_module::instance($cm->id); $isbranded = component_callback('mod_' . $cm->modname, 'is_branded', [], false); // Common info (for people being able to see the module or availability dates). $module['id'] = $cm->id; $module['name'] = \core_external\util::format_string($cm->name, $modcontext); $module['instance'] = $cm->instance; $module['contextid'] = $modcontext->id; $module['modname'] = (string) $cm->modname; $module['modplural'] = (string) $cm->modplural; $module['modicon'] = $cm->get_icon_url()->out(false); $module['purpose'] = plugin_supports('mod', $cm->modname, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER); $module['branded'] = $isbranded; $module['indent'] = $cm->indent; $module['onclick'] = $cm->onclick; $module['afterlink'] = $cm->afterlink; $activitybadgedata = $cm->get_activitybadge(); if (!empty($activitybadgedata)) { $module['activitybadge'] = $activitybadgedata; } $module['customdata'] = json_encode($cm->customdata); $module['completion'] = $cm->completion; $module['downloadcontent'] = $cm->downloadcontent; $module['noviewlink'] = plugin_supports('mod', $cm->modname, FEATURE_NO_VIEW_LINK, false); $module['candisplay'] = plugin_supports('mod', $cm->modname, FEATURE_CAN_DISPLAY, true); $module['dates'] = $activitydates; $module['groupmode'] = $cm->groupmode; // Check module completion. $completion = $completioninfo->is_enabled($cm); if ($completion != COMPLETION_DISABLED) { $exporter = new \core_completion\external\completion_info_exporter($course, $cm, $USER->id); $renderer = $PAGE->get_renderer('core'); $modulecompletiondata = (array)$exporter->export($renderer); $module['completiondata'] = $modulecompletiondata; } if (!empty($cm->showdescription) or $module['noviewlink']) { // We want to use the external format. However from reading get_formatted_content(), $cm->content format is always FORMAT_HTML. $options = array('noclean' => true); list($module['description'], $descriptionformat) = \core_external\util::format_text($cm->content, FORMAT_HTML, $modcontext, $cm->modname, 'intro', $cm->id, $options); } //url of the module $url = $cm->url; if ($url) { //labels don't have url $module['url'] = $url->out(false); } $canviewhidden = has_capability('moodle/course:viewhiddenactivities', context_module::instance($cm->id)); //user that can view hidden module should know about the visibility $module['visible'] = $cm->visible; $module['visibleoncoursepage'] = $cm->visibleoncoursepage; $module['uservisible'] = $cm->uservisible; if (!empty($cm->availableinfo)) { $module['availabilityinfo'] = \core_availability\info::format_info($cm->availableinfo, $course); } // Availability date (also send to user who can see hidden module). if ($CFG->enableavailability && ($canviewhidden || $canupdatecourse)) { $module['availability'] = $cm->availability; } // Return contents only if the user can access to the module. if ($cm->uservisible) { $baseurl = 'webservice/pluginfile.php'; // Call $modulename_export_contents (each module callback take care about checking the capabilities). require_once($CFG->dirroot . '/mod/' . $cm->modname . '/lib.php'); $getcontentfunction = $cm->modname.'_export_contents'; if (function_exists($getcontentfunction)) { $contents = $getcontentfunction($cm, $baseurl); $module['contentsinfo'] = array( 'filescount' => count($contents), 'filessize' => 0, 'lastmodified' => 0, 'mimetypes' => array(), ); foreach ($contents as $content) { // Check repository file (only main file). if (!isset($module['contentsinfo']['repositorytype'])) { $module['contentsinfo']['repositorytype'] = isset($content['repositorytype']) ? $content['repositorytype'] : ''; } if (isset($content['filesize'])) { $module['contentsinfo']['filessize'] += $content['filesize']; } if (isset($content['timemodified']) && ($content['timemodified'] > $module['contentsinfo']['lastmodified'])) { $module['contentsinfo']['lastmodified'] = $content['timemodified']; } if (isset($content['mimetype'])) { $module['contentsinfo']['mimetypes'][$content['mimetype']] = $content['mimetype']; } } if (empty($filters['excludecontents']) and !empty($contents)) { $module['contents'] = $contents; } else { $module['contents'] = array(); } } } // Assign result to $sectioncontents, there is an exception, // stealth activities in non-visible sections for students go to a special section. if (!empty($filters['includestealthmodules']) && !$section->uservisible && $cm->is_stealth()) { $stealthmodules[] = $module; } else { $sectioncontents[] = $module; } // If we just did a filtering, break the loop. if ($modfound) { break; } } } $sectionvalues['modules'] = $sectioncontents; // assign result to $coursecontents $coursecontents[$key] = $sectionvalues; // Break the loop if we are filtering. if ($sectionfound) { break; } } // Now that we have iterated over all the sections and activities, check the visibility. // We didn't this before to be able to retrieve stealth activities. foreach ($coursecontents as $sectionnumber => $sectioncontents) { $section = $sections[$sectionnumber]; if (!$courseformat->is_section_visible($section)) { unset($coursecontents[$sectionnumber]); continue; } // Remove section and modules information if the section is not visible for the user. if (!$section->uservisible) { $coursecontents[$sectionnumber]['modules'] = array(); // Remove summary information if the section is completely hidden only, // even if the section is not user visible, the summary is always displayed among the availability information. if (!$section->visible) { $coursecontents[$sectionnumber]['summary'] = ''; } } } // Include stealth modules in special section (without any info). if (!empty($stealthmodules)) { $coursecontents[] = array( 'id' => -1, 'name' => '', 'summary' => '', 'summaryformat' => FORMAT_MOODLE, 'modules' => $stealthmodules ); } } return $coursecontents; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.2 */ public static function get_course_contents_returns() { $completiondefinition = \core_completion\external\completion_info_exporter::get_read_structure(VALUE_DEFAULT, []); return new external_multiple_structure( new external_single_structure( [ 'id' => new external_value(PARAM_INT, 'Section ID'), 'name' => new external_value(PARAM_RAW, 'Section name'), 'visible' => new external_value(PARAM_INT, 'is the section visible', VALUE_OPTIONAL), 'summary' => new external_value(PARAM_RAW, 'Section description'), 'summaryformat' => new external_format_value('summary'), 'section' => new external_value(PARAM_INT, 'Section number inside the course', VALUE_OPTIONAL), 'hiddenbynumsections' => new external_value( PARAM_INT, 'Whether is a section hidden in the course format', VALUE_OPTIONAL, ), 'uservisible' => new external_value(PARAM_BOOL, 'Is the section visible for the user?', VALUE_OPTIONAL), 'availabilityinfo' => new external_value(PARAM_RAW, 'Availability information.', VALUE_OPTIONAL), 'component' => new external_value( PARAM_COMPONENT, 'The delegate component of this section if any.', VALUE_OPTIONAL, ), 'itemid' => new external_value( PARAM_INT, 'The optional item id delegate component can use to identify its instance.', VALUE_OPTIONAL, ), 'modules' => new external_multiple_structure( new external_single_structure( [ 'id' => new external_value(PARAM_INT, 'activity id'), 'url' => new external_value(PARAM_URL, 'activity url', VALUE_OPTIONAL), 'name' => new external_value(PARAM_RAW, 'activity module name'), 'instance' => new external_value(PARAM_INT, 'instance id', VALUE_OPTIONAL), 'contextid' => new external_value(PARAM_INT, 'Activity context id.', VALUE_OPTIONAL), 'description' => new external_value(PARAM_RAW, 'activity description', VALUE_OPTIONAL), 'visible' => new external_value(PARAM_INT, 'is the module visible', VALUE_OPTIONAL), 'uservisible' => new external_value( PARAM_BOOL, 'Is the module visible for the user?', VALUE_OPTIONAL, ), 'availabilityinfo' => new external_value( PARAM_RAW, 'Availability information.', VALUE_OPTIONAL, ), 'visibleoncoursepage' => new external_value( PARAM_INT, 'is the module visible on course page', VALUE_OPTIONAL, ), 'modicon' => new external_value(PARAM_URL, 'activity icon url'), 'modname' => new external_value(PARAM_PLUGIN, 'activity module type'), 'purpose' => new external_value(PARAM_ALPHA, 'the module purpose'), 'branded' => new external_value( PARAM_BOOL, 'Whether the module is branded or not', VALUE_OPTIONAL, ), 'modplural' => new external_value(PARAM_TEXT, 'activity module plural name'), 'availability' => new external_value(PARAM_RAW, 'module availability settings', VALUE_OPTIONAL), 'indent' => new external_value(PARAM_INT, 'number of identation in the site'), 'onclick' => new external_value(PARAM_RAW, 'Onclick action.', VALUE_OPTIONAL), 'afterlink' => new external_value( PARAM_RAW, 'After link info to be displayed.', VALUE_OPTIONAL, ), 'activitybadge' => self::get_activitybadge_structure(), 'customdata' => new external_value(PARAM_RAW, 'Custom data (JSON encoded).', VALUE_OPTIONAL), 'noviewlink' => new external_value( PARAM_BOOL, 'Whether the module has no view page', VALUE_OPTIONAL, ), 'candisplay' => new external_value( PARAM_BOOL, 'Whether the module should be displayed on the course page', VALUE_OPTIONAL, ), 'completion' => new external_value( PARAM_INT, 'Type of completion tracking: 0 means none, 1 manual, 2 automatic.', VALUE_OPTIONAL, ), 'completiondata' => $completiondefinition, 'downloadcontent' => new external_value(PARAM_INT, 'The download content value', VALUE_OPTIONAL), 'dates' => new external_multiple_structure( new external_single_structure( [ 'label' => new external_value(PARAM_TEXT, 'date label'), 'timestamp' => new external_value(PARAM_INT, 'date timestamp'), 'relativeto' => new external_value( PARAM_INT, 'relative date timestamp', VALUE_OPTIONAL, ), 'dataid' => new external_value(PARAM_NOTAGS, 'cm data id', VALUE_OPTIONAL), ], ), 'Course dates', VALUE_DEFAULT, [], ), 'groupmode' => new external_value(PARAM_INT, 'Group mode value', VALUE_OPTIONAL), 'contents' => new external_multiple_structure( new external_single_structure( [ // Content info. 'type' => new external_value(PARAM_TEXT, 'a file or a folder or external link'), 'filename' => new external_value(PARAM_FILE, 'filename'), 'filepath' => new external_value(PARAM_PATH, 'filepath'), 'filesize' => new external_value(PARAM_INT, 'filesize'), 'fileurl' => new external_value(PARAM_URL, 'downloadable file url', VALUE_OPTIONAL), 'content' => new external_value( PARAM_RAW, 'Raw content, will be used when type is content', VALUE_OPTIONAL, ), 'timecreated' => new external_value(PARAM_INT, 'Time created'), 'timemodified' => new external_value(PARAM_INT, 'Time modified'), 'sortorder' => new external_value(PARAM_INT, 'Content sort order'), 'mimetype' => new external_value( PARAM_RAW, 'File mime type.', VALUE_OPTIONAL, ), 'isexternalfile' => new external_value( PARAM_BOOL, 'Whether is an external file.', VALUE_OPTIONAL, ), 'repositorytype' => new external_value( PARAM_PLUGIN, 'The repository type for external files.', VALUE_OPTIONAL, ), // Copyright related info. 'userid' => new external_value(PARAM_INT, 'User who added this content to moodle'), 'author' => new external_value(PARAM_TEXT, 'Content owner'), 'license' => new external_value(PARAM_TEXT, 'Content license'), 'tags' => new external_multiple_structure( \core_tag\external\tag_item_exporter::get_read_structure(), 'Tags', VALUE_OPTIONAL, ), ], ), 'Course contents', VALUE_DEFAULT, [], ), 'contentsinfo' => new external_single_structure( [ 'filescount' => new external_value(PARAM_INT, 'Total number of files.'), 'filessize' => new external_value(PARAM_INT, 'Total files size.'), 'lastmodified' => new external_value(PARAM_INT, 'Last time files were modified.'), 'mimetypes' => new external_multiple_structure( new external_value(PARAM_RAW, 'File mime type.'), 'Files mime types.' ), 'repositorytype' => new external_value( PARAM_PLUGIN, 'The repository type for the main file.', VALUE_OPTIONAL, ), ], 'Contents summary information.', VALUE_OPTIONAL, ), ], ), 'list of modules', ), ], ), ); } /** * Returns description of activitybadge data. * * @return external_description */ protected static function get_activitybadge_structure(): external_description { return new external_single_structure( [ 'badgecontent' => new external_value( PARAM_TEXT, 'The content to be displayed in the activity badge', VALUE_OPTIONAL ), 'badgestyle' => new external_value( PARAM_TEXT, 'The style for the activity badge', VALUE_OPTIONAL ), 'badgeurl' => new external_value( PARAM_URL, 'An optional URL to redirect the user when the activity badge is clicked', VALUE_OPTIONAL ), 'badgeelementid' => new external_value( PARAM_ALPHANUMEXT, 'An optional id in case the module wants to add some code for the activity badge', VALUE_OPTIONAL ), 'badgeextraattributes' => new external_multiple_structure( new external_single_structure( [ 'name' => new external_value( PARAM_TEXT, 'The attribute name', VALUE_OPTIONAL ), 'value' => new external_value( PARAM_TEXT, 'The attribute value', VALUE_OPTIONAL ), ], 'Each of the attribute names and values', VALUE_OPTIONAL ), 'An optional array of extra HTML attributes to add to the badge element', VALUE_OPTIONAL ), ], 'Activity badge to display near the name', VALUE_OPTIONAL ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function get_courses_parameters() { return new external_function_parameters( array('options' => new external_single_structure( array('ids' => new external_multiple_structure( new external_value(PARAM_INT, 'Course id') , 'List of course id. If empty return all courses except front page course.', VALUE_OPTIONAL) ), 'options - operator OR is used', VALUE_DEFAULT, array()) ) ); } /** * Get courses * * @param array $options It contains an array (list of ids) * @return array * @since Moodle 2.2 */ public static function get_courses($options = array()) { global $CFG, $DB; require_once($CFG->dirroot . "/course/lib.php"); //validate parameter $params = self::validate_parameters(self::get_courses_parameters(), array('options' => $options)); //retrieve courses if (!array_key_exists('ids', $params['options']) or empty($params['options']['ids'])) { $courses = $DB->get_records('course'); } else { $courses = $DB->get_records_list('course', 'id', $params['options']['ids']); } //create return value $coursesinfo = array(); foreach ($courses as $course) { // now security checks $context = context_course::instance($course->id, IGNORE_MISSING); $courseformatoptions = course_get_format($course)->get_format_options(); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->courseid = $course->id; throw new moodle_exception('errorcoursecontextnotvalid', 'webservice', '', $exceptionparam); } if ($course->id != SITEID) { require_capability('moodle/course:view', $context); } $courseinfo = array(); $courseinfo['id'] = $course->id; $courseinfo['fullname'] = \core_external\util::format_string($course->fullname, $context); $courseinfo['shortname'] = \core_external\util::format_string($course->shortname, $context); $courseinfo['displayname'] = \core_external\util::format_string(get_course_display_name_for_list($course), $context); $courseinfo['categoryid'] = $course->category; list($courseinfo['summary'], $courseinfo['summaryformat']) = \core_external\util::format_text($course->summary, $course->summaryformat, $context, 'course', 'summary', 0); $courseinfo['format'] = $course->format; $courseinfo['startdate'] = $course->startdate; $courseinfo['enddate'] = $course->enddate; $courseinfo['showactivitydates'] = $course->showactivitydates; $courseinfo['showcompletionconditions'] = $course->showcompletionconditions; if (array_key_exists('numsections', $courseformatoptions)) { // For backward-compartibility $courseinfo['numsections'] = $courseformatoptions['numsections']; } $courseinfo['pdfexportfont'] = $course->pdfexportfont; $handler = core_course\customfield\course_handler::create(); if ($customfields = $handler->export_instance_data($course->id)) { $courseinfo['customfields'] = []; foreach ($customfields as $data) { $courseinfo['customfields'][] = [ 'type' => $data->get_type(), 'value' => $data->get_value(), 'valueraw' => $data->get_data_controller()->get_value(), 'name' => $data->get_name(), 'shortname' => $data->get_shortname() ]; } } //some field should be returned only if the user has update permission $courseadmin = has_capability('moodle/course:update', $context); if ($courseadmin) { $courseinfo['categorysortorder'] = $course->sortorder; $courseinfo['idnumber'] = $course->idnumber; $courseinfo['showgrades'] = $course->showgrades; $courseinfo['showreports'] = $course->showreports; $courseinfo['newsitems'] = $course->newsitems; $courseinfo['visible'] = $course->visible; $courseinfo['maxbytes'] = $course->maxbytes; if (array_key_exists('hiddensections', $courseformatoptions)) { // For backward-compartibility $courseinfo['hiddensections'] = $courseformatoptions['hiddensections']; } // Return numsections for backward-compatibility with clients who expect it. $courseinfo['numsections'] = course_get_format($course)->get_last_section_number(); $courseinfo['groupmode'] = $course->groupmode; $courseinfo['groupmodeforce'] = $course->groupmodeforce; $courseinfo['defaultgroupingid'] = $course->defaultgroupingid; $courseinfo['lang'] = clean_param($course->lang, PARAM_LANG); $courseinfo['timecreated'] = $course->timecreated; $courseinfo['timemodified'] = $course->timemodified; $courseinfo['forcetheme'] = clean_param($course->theme, PARAM_THEME); $courseinfo['enablecompletion'] = $course->enablecompletion; $courseinfo['completionnotify'] = $course->completionnotify; $courseinfo['courseformatoptions'] = array(); foreach ($courseformatoptions as $key => $value) { $courseinfo['courseformatoptions'][] = array( 'name' => $key, 'value' => $value ); } } if ($courseadmin or $course->visible or has_capability('moodle/course:viewhiddencourses', $context)) { $coursesinfo[] = $courseinfo; } } return $coursesinfo; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.2 */ public static function get_courses_returns() { return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'course id'), 'shortname' => new external_value(PARAM_RAW, 'course short name'), 'categoryid' => new external_value(PARAM_INT, 'category id'), 'categorysortorder' => new external_value(PARAM_INT, 'sort order into the category', VALUE_OPTIONAL), 'fullname' => new external_value(PARAM_RAW, 'full name'), 'displayname' => new external_value(PARAM_RAW, 'course display name'), 'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL), 'summary' => new external_value(PARAM_RAW, 'summary'), 'summaryformat' => new external_format_value('summary'), 'format' => new external_value(PARAM_PLUGIN, 'course format: weeks, topics, social, site,..'), 'showgrades' => new external_value(PARAM_INT, '1 if grades are shown, otherwise 0', VALUE_OPTIONAL), 'newsitems' => new external_value(PARAM_INT, 'number of recent items appearing on the course page', VALUE_OPTIONAL), 'startdate' => new external_value(PARAM_INT, 'timestamp when the course start'), 'enddate' => new external_value(PARAM_INT, 'timestamp when the course end'), 'numsections' => new external_value(PARAM_INT, '(deprecated, use courseformatoptions) number of weeks/topics', VALUE_OPTIONAL), 'maxbytes' => new external_value(PARAM_INT, 'largest size of file that can be uploaded into the course', VALUE_OPTIONAL), 'showreports' => new external_value(PARAM_INT, 'are activity report shown (yes = 1, no =0)', VALUE_OPTIONAL), 'visible' => new external_value(PARAM_INT, '1: available to student, 0:not available', VALUE_OPTIONAL), 'hiddensections' => new external_value(PARAM_INT, '(deprecated, use courseformatoptions) How the hidden sections in the course are displayed to students', VALUE_OPTIONAL), 'groupmode' => new external_value(PARAM_INT, 'no group, separate, visible', VALUE_OPTIONAL), 'groupmodeforce' => new external_value(PARAM_INT, '1: yes, 0: no', VALUE_OPTIONAL), 'defaultgroupingid' => new external_value(PARAM_INT, 'default grouping id', VALUE_OPTIONAL), 'timecreated' => new external_value(PARAM_INT, 'timestamp when the course have been created', VALUE_OPTIONAL), 'timemodified' => new external_value(PARAM_INT, 'timestamp when the course have been modified', VALUE_OPTIONAL), 'enablecompletion' => new external_value(PARAM_INT, 'Enabled, control via completion and activity settings. Disbaled, not shown in activity settings.', VALUE_OPTIONAL), 'completionnotify' => new external_value(PARAM_INT, '1: yes 0: no', VALUE_OPTIONAL), 'lang' => new external_value(PARAM_SAFEDIR, 'forced course language', VALUE_OPTIONAL), 'forcetheme' => new external_value(PARAM_PLUGIN, 'name of the force theme', VALUE_OPTIONAL), 'courseformatoptions' => new external_multiple_structure( new external_single_structure( array('name' => new external_value(PARAM_ALPHANUMEXT, 'course format option name'), 'value' => new external_value(PARAM_RAW, 'course format option value') )), 'additional options for particular course format', VALUE_OPTIONAL ), 'showactivitydates' => new external_value(PARAM_BOOL, 'Whether the activity dates are shown or not'), 'showcompletionconditions' => new external_value(PARAM_BOOL, 'Whether the activity completion conditions are shown or not'), 'customfields' => new external_multiple_structure( new external_single_structure( ['name' => new external_value(PARAM_RAW, 'The name of the custom field'), 'shortname' => new external_value(PARAM_ALPHANUMEXT, 'The shortname of the custom field'), 'type' => new external_value(PARAM_COMPONENT, 'The type of the custom field - text, checkbox...'), 'valueraw' => new external_value(PARAM_RAW, 'The raw value of the custom field'), 'value' => new external_value(PARAM_RAW, 'The value of the custom field')] ), 'Custom fields and associated values', VALUE_OPTIONAL), ), 'course' ) ); } /** * Return array of all editable course custom fields indexed by their shortname * * @param \context $context * @param int $courseid * @return \core_customfield\field_controller[] */ public static function get_editable_customfields(\context $context, int $courseid = 0): array { $result = []; $handler = \core_course\customfield\course_handler::create(); $handler->set_parent_context($context); foreach ($handler->get_editable_fields($courseid) as $field) { $result[$field->get('shortname')] = $field; } return $result; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function create_courses_parameters() { $courseconfig = get_config('moodlecourse'); //needed for many default values return new external_function_parameters( array( 'courses' => new external_multiple_structure( new external_single_structure( array( 'fullname' => new external_value(PARAM_TEXT, 'full name'), 'shortname' => new external_value(PARAM_TEXT, 'course short name'), 'categoryid' => new external_value(PARAM_INT, 'category id'), 'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL), 'summary' => new external_value(PARAM_RAW, 'summary', VALUE_OPTIONAL), 'summaryformat' => new external_format_value('summary', VALUE_DEFAULT), 'format' => new external_value(PARAM_PLUGIN, 'course format: weeks, topics, social, site,..', VALUE_DEFAULT, $courseconfig->format), 'showgrades' => new external_value(PARAM_INT, '1 if grades are shown, otherwise 0', VALUE_DEFAULT, $courseconfig->showgrades), 'newsitems' => new external_value(PARAM_INT, 'number of recent items appearing on the course page', VALUE_DEFAULT, $courseconfig->newsitems), 'startdate' => new external_value(PARAM_INT, 'timestamp when the course start', VALUE_OPTIONAL), 'enddate' => new external_value(PARAM_INT, 'timestamp when the course end', VALUE_OPTIONAL), 'numsections' => new external_value(PARAM_INT, '(deprecated, use courseformatoptions) number of weeks/topics', VALUE_OPTIONAL), 'maxbytes' => new external_value(PARAM_INT, 'largest size of file that can be uploaded into the course', VALUE_DEFAULT, $courseconfig->maxbytes), 'showreports' => new external_value(PARAM_INT, 'are activity report shown (yes = 1, no =0)', VALUE_DEFAULT, $courseconfig->showreports), 'visible' => new external_value( PARAM_INT, '1: available to student, 0: not available', VALUE_DEFAULT, $courseconfig->visible, ), 'hiddensections' => new external_value(PARAM_INT, '(deprecated, use courseformatoptions) How the hidden sections in the course are displayed to students', VALUE_OPTIONAL), 'groupmode' => new external_value(PARAM_INT, 'no group, separate, visible', VALUE_DEFAULT, $courseconfig->groupmode), 'groupmodeforce' => new external_value(PARAM_INT, '1: yes, 0: no', VALUE_DEFAULT, $courseconfig->groupmodeforce), 'defaultgroupingid' => new external_value(PARAM_INT, 'default grouping id', VALUE_DEFAULT, 0), 'enablecompletion' => new external_value(PARAM_INT, 'Enabled, control via completion and activity settings. Disabled, not shown in activity settings.', VALUE_OPTIONAL), 'completionnotify' => new external_value(PARAM_INT, '1: yes 0: no', VALUE_OPTIONAL), 'lang' => new external_value(PARAM_SAFEDIR, 'forced course language', VALUE_OPTIONAL), 'forcetheme' => new external_value(PARAM_PLUGIN, 'name of the force theme', VALUE_OPTIONAL), 'courseformatoptions' => new external_multiple_structure( new external_single_structure( array('name' => new external_value(PARAM_ALPHANUMEXT, 'course format option name'), 'value' => new external_value(PARAM_RAW, 'course format option value') )), 'additional options for particular course format', VALUE_OPTIONAL), 'customfields' => new external_multiple_structure( new external_single_structure( array( 'shortname' => new external_value(PARAM_ALPHANUMEXT, 'The shortname of the custom field'), 'value' => new external_value(PARAM_RAW, 'The value of the custom field'), )), 'custom fields for the course', VALUE_OPTIONAL ) )), 'courses to create' ) ) ); } /** * Create courses * * @param array $courses * @return array courses (id and shortname only) * @since Moodle 2.2 */ public static function create_courses($courses) { global $CFG, $DB; require_once($CFG->dirroot . "/course/lib.php"); require_once($CFG->libdir . '/completionlib.php'); $params = self::validate_parameters(self::create_courses_parameters(), array('courses' => $courses)); $courseconfig = get_config('moodlecourse'); $availablethemes = core_component::get_plugin_list('theme'); $availablelangs = get_string_manager()->get_list_of_translations(); $transaction = $DB->start_delegated_transaction(); foreach ($params['courses'] as $course) { // Ensure the current user is allowed to run this function $context = context_coursecat::instance($course['categoryid'], IGNORE_MISSING); try { self::validate_context($context); } catch (Exception $e) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->catid = $course['categoryid']; throw new moodle_exception('errorcatcontextnotvalid', 'webservice', '', $exceptionparam); } require_capability('moodle/course:create', $context); // Fullname and short name are required to be non-empty. if (trim($course['fullname']) === '') { throw new moodle_exception('errorinvalidparam', 'webservice', '', 'fullname'); } else if (trim($course['shortname']) === '') { throw new moodle_exception('errorinvalidparam', 'webservice', '', 'shortname'); } // Make sure start/end date are correctly set. if (!array_key_exists('startdate', $course)) { $course['startdate'] = usergetmidnight(time()); } if (!array_key_exists('enddate', $course) && $courseconfig->courseenddateenabled) { $course['enddate'] = $course['startdate'] + $courseconfig->courseduration; } // Make sure lang is valid if (array_key_exists('lang', $course)) { if (empty($availablelangs[$course['lang']])) { throw new moodle_exception('errorinvalidparam', 'webservice', '', 'lang'); } if (!has_capability('moodle/course:setforcedlanguage', $context)) { unset($course['lang']); } } // Make sure theme is valid if (array_key_exists('forcetheme', $course)) { if (!empty($CFG->allowcoursethemes)) { if (empty($availablethemes[$course['forcetheme']])) { throw new moodle_exception('errorinvalidparam', 'webservice', '', 'forcetheme'); } else { $course['theme'] = $course['forcetheme']; } } } $course['showactivitydates'] = $courseconfig->showactivitydates; //force visibility if ws user doesn't have the permission to set it $category = $DB->get_record('course_categories', array('id' => $course['categoryid'])); if (!has_capability('moodle/course:visibility', $context)) { $course['visible'] = $category->visible; } //set default value for completion if (completion_info::is_enabled_for_site()) { if (!array_key_exists('enablecompletion', $course)) { $course['enablecompletion'] = $courseconfig->enablecompletion; } } else { $course['enablecompletion'] = 0; } $course['category'] = $course['categoryid']; // Summary format. $course['summaryformat'] = util::validate_format($course['summaryformat']); if (!empty($course['courseformatoptions'])) { foreach ($course['courseformatoptions'] as $option) { $course[$option['name']] = $option['value']; } } // Custom fields. if (!empty($course['customfields'])) { $customfields = self::get_editable_customfields($context); foreach ($course['customfields'] as $field) { if (array_key_exists($field['shortname'], $customfields)) { // Ensure we're populating the element form fields correctly. $controller = \core_customfield\data_controller::create(0, null, $customfields[$field['shortname']]); $course[$controller->get_form_element_name()] = $field['value']; } } } //Note: create_course() core function check shortname, idnumber, category $course['id'] = create_course((object) $course)->id; $resultcourses[] = array('id' => $course['id'], 'shortname' => $course['shortname']); } $transaction->allow_commit(); return $resultcourses; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.2 */ public static function create_courses_returns() { return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'course id'), 'shortname' => new external_value(PARAM_RAW, 'short name'), ) ) ); } /** * Update courses * * @return external_function_parameters * @since Moodle 2.5 */ public static function update_courses_parameters() { return new external_function_parameters( array( 'courses' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'ID of the course'), 'fullname' => new external_value(PARAM_TEXT, 'full name', VALUE_OPTIONAL), 'shortname' => new external_value(PARAM_TEXT, 'course short name', VALUE_OPTIONAL), 'categoryid' => new external_value(PARAM_INT, 'category id', VALUE_OPTIONAL), 'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL), 'summary' => new external_value(PARAM_RAW, 'summary', VALUE_OPTIONAL), 'summaryformat' => new external_format_value('summary', VALUE_OPTIONAL), 'format' => new external_value(PARAM_PLUGIN, 'course format: weeks, topics, social, site,..', VALUE_OPTIONAL), 'showgrades' => new external_value(PARAM_INT, '1 if grades are shown, otherwise 0', VALUE_OPTIONAL), 'newsitems' => new external_value(PARAM_INT, 'number of recent items appearing on the course page', VALUE_OPTIONAL), 'startdate' => new external_value(PARAM_INT, 'timestamp when the course start', VALUE_OPTIONAL), 'enddate' => new external_value(PARAM_INT, 'timestamp when the course end', VALUE_OPTIONAL), 'numsections' => new external_value(PARAM_INT, '(deprecated, use courseformatoptions) number of weeks/topics', VALUE_OPTIONAL), 'maxbytes' => new external_value(PARAM_INT, 'largest size of file that can be uploaded into the course', VALUE_OPTIONAL), 'showreports' => new external_value(PARAM_INT, 'are activity report shown (yes = 1, no =0)', VALUE_OPTIONAL), 'visible' => new external_value(PARAM_INT, '1: available to student, 0:not available', VALUE_OPTIONAL), 'hiddensections' => new external_value(PARAM_INT, '(deprecated, use courseformatoptions) How the hidden sections in the course are displayed to students', VALUE_OPTIONAL), 'groupmode' => new external_value(PARAM_INT, 'no group, separate, visible', VALUE_OPTIONAL), 'groupmodeforce' => new external_value(PARAM_INT, '1: yes, 0: no', VALUE_OPTIONAL), 'defaultgroupingid' => new external_value(PARAM_INT, 'default grouping id', VALUE_OPTIONAL), 'enablecompletion' => new external_value(PARAM_INT, 'Enabled, control via completion and activity settings. Disabled, not shown in activity settings.', VALUE_OPTIONAL), 'completionnotify' => new external_value(PARAM_INT, '1: yes 0: no', VALUE_OPTIONAL), 'lang' => new external_value(PARAM_SAFEDIR, 'forced course language', VALUE_OPTIONAL), 'forcetheme' => new external_value(PARAM_PLUGIN, 'name of the force theme', VALUE_OPTIONAL), 'courseformatoptions' => new external_multiple_structure( new external_single_structure( array('name' => new external_value(PARAM_ALPHANUMEXT, 'course format option name'), 'value' => new external_value(PARAM_RAW, 'course format option value') )), 'additional options for particular course format', VALUE_OPTIONAL), 'customfields' => new external_multiple_structure( new external_single_structure( [ 'shortname' => new external_value(PARAM_ALPHANUMEXT, 'The shortname of the custom field'), 'value' => new external_value(PARAM_RAW, 'The value of the custom field') ] ), 'Custom fields', VALUE_OPTIONAL), ) ), 'courses to update' ) ) ); } /** * Update courses * * @param array $courses * @since Moodle 2.5 */ public static function update_courses($courses) { global $CFG, $DB; require_once($CFG->dirroot . "/course/lib.php"); $warnings = array(); $params = self::validate_parameters(self::update_courses_parameters(), array('courses' => $courses)); $availablethemes = core_component::get_plugin_list('theme'); $availablelangs = get_string_manager()->get_list_of_translations(); foreach ($params['courses'] as $course) { // Catch any exception while updating course and return as warning to user. try { // Ensure the current user is allowed to run this function. $context = context_course::instance($course['id'], MUST_EXIST); self::validate_context($context); $oldcourse = course_get_format($course['id'])->get_course(); require_capability('moodle/course:update', $context); // Check if user can change category. if (array_key_exists('categoryid', $course) && ($oldcourse->category != $course['categoryid'])) { require_capability('moodle/course:changecategory', $context); $course['category'] = $course['categoryid']; } // Check if the user can change fullname, and the new value is non-empty. if (array_key_exists('fullname', $course) && ($oldcourse->fullname != $course['fullname'])) { require_capability('moodle/course:changefullname', $context); if (trim($course['fullname']) === '') { throw new moodle_exception('errorinvalidparam', 'webservice', '', 'fullname'); } } // Check if the user can change shortname, and the new value is non-empty. if (array_key_exists('shortname', $course) && ($oldcourse->shortname != $course['shortname'])) { require_capability('moodle/course:changeshortname', $context); if (trim($course['shortname']) === '') { throw new moodle_exception('errorinvalidparam', 'webservice', '', 'shortname'); } } // Check if the user can change the idnumber. if (array_key_exists('idnumber', $course) && ($oldcourse->idnumber != $course['idnumber'])) { require_capability('moodle/course:changeidnumber', $context); } // Check if user can change summary. if (array_key_exists('summary', $course) && ($oldcourse->summary != $course['summary'])) { require_capability('moodle/course:changesummary', $context); } // Summary format. if (array_key_exists('summaryformat', $course) && ($oldcourse->summaryformat != $course['summaryformat'])) { require_capability('moodle/course:changesummary', $context); $course['summaryformat'] = util::validate_format($course['summaryformat']); } // Check if user can change visibility. if (array_key_exists('visible', $course) && ($oldcourse->visible != $course['visible'])) { require_capability('moodle/course:visibility', $context); } // Make sure lang is valid. if (array_key_exists('lang', $course) && ($oldcourse->lang != $course['lang'])) { require_capability('moodle/course:setforcedlanguage', $context); if (empty($availablelangs[$course['lang']])) { throw new moodle_exception('errorinvalidparam', 'webservice', '', 'lang'); } } // Make sure theme is valid. if (array_key_exists('forcetheme', $course)) { if (!empty($CFG->allowcoursethemes)) { if (empty($availablethemes[$course['forcetheme']])) { throw new moodle_exception('errorinvalidparam', 'webservice', '', 'forcetheme'); } else { $course['theme'] = $course['forcetheme']; } } } // Make sure completion is enabled before setting it. if (array_key_exists('enabledcompletion', $course) && !completion_info::is_enabled_for_site()) { $course['enabledcompletion'] = 0; } // Make sure maxbytes are less then CFG->maxbytes. if (array_key_exists('maxbytes', $course)) { // We allow updates back to 0 max bytes, a special value denoting the course uses the site limit. // Otherwise, either use the size specified, or cap at the max size for the course. if ($course['maxbytes'] != 0) { $course['maxbytes'] = get_max_upload_file_size($CFG->maxbytes, $course['maxbytes']); } } if (!empty($course['courseformatoptions'])) { foreach ($course['courseformatoptions'] as $option) { if (isset($option['name']) && isset($option['value'])) { $course[$option['name']] = $option['value']; } } } // Custom fields. if (isset($course['customfields'])) { $customfields = self::get_editable_customfields($context, $course['id']); foreach ($course['customfields'] as $field) { if (array_key_exists($field['shortname'], $customfields)) { // Ensure we're populating the element form fields correctly. $controller = \core_customfield\data_controller::create(0, null, $customfields[$field['shortname']]); $course[$controller->get_form_element_name()] = $field['value']; } } } // Update course if user has all required capabilities. update_course((object) $course); } catch (Exception $e) { $warning = array(); $warning['item'] = 'course'; $warning['itemid'] = $course['id']; if ($e instanceof moodle_exception) { $warning['warningcode'] = $e->errorcode; } else { $warning['warningcode'] = $e->getCode(); } $warning['message'] = $e->getMessage(); $warnings[] = $warning; } } $result = array(); $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.5 */ public static function update_courses_returns() { return new external_single_structure( array( 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function delete_courses_parameters() { return new external_function_parameters( array( 'courseids' => new external_multiple_structure(new external_value(PARAM_INT, 'course ID')), ) ); } /** * Delete courses * * @param array $courseids A list of course ids * @since Moodle 2.2 */ public static function delete_courses($courseids) { global $CFG, $DB; require_once($CFG->dirroot."/course/lib.php"); // Parameter validation. $params = self::validate_parameters(self::delete_courses_parameters(), array('courseids'=>$courseids)); $warnings = array(); foreach ($params['courseids'] as $courseid) { $course = $DB->get_record('course', array('id' => $courseid)); if ($course === false) { $warnings[] = array( 'item' => 'course', 'itemid' => $courseid, 'warningcode' => 'unknowncourseidnumber', 'message' => 'Unknown course ID ' . $courseid ); continue; } // Check if the context is valid. $coursecontext = context_course::instance($course->id); self::validate_context($coursecontext); // Check if the current user has permission. if (!can_delete_course($courseid)) { $warnings[] = array( 'item' => 'course', 'itemid' => $courseid, 'warningcode' => 'cannotdeletecourse', 'message' => 'You do not have the permission to delete this course' . $courseid ); continue; } if (delete_course($course, false) === false) { $warnings[] = array( 'item' => 'course', 'itemid' => $courseid, 'warningcode' => 'cannotdeletecategorycourse', 'message' => 'Course ' . $courseid . ' failed to be deleted' ); continue; } } fix_course_sortorder(); return array('warnings' => $warnings); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.2 */ public static function delete_courses_returns() { return new external_single_structure( array( 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function duplicate_course_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'course to duplicate id'), 'fullname' => new external_value(PARAM_TEXT, 'duplicated course full name'), 'shortname' => new external_value(PARAM_TEXT, 'duplicated course short name'), 'categoryid' => new external_value(PARAM_INT, 'duplicated course category parent'), 'visible' => new external_value(PARAM_INT, 'duplicated course visible, default to yes', VALUE_DEFAULT, 1), 'options' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_ALPHAEXT, 'The backup option name: "activities" (int) Include course activites (default to 1 that is equal to yes), "blocks" (int) Include course blocks (default to 1 that is equal to yes), "filters" (int) Include course filters (default to 1 that is equal to yes), "users" (int) Include users (default to 0 that is equal to no), "enrolments" (int) Include enrolment methods (default to 1 - restore only with users), "role_assignments" (int) Include role assignments (default to 0 that is equal to no), "comments" (int) Include user comments (default to 0 that is equal to no), "userscompletion" (int) Include user course completion information (default to 0 that is equal to no), "logs" (int) Include course logs (default to 0 that is equal to no), "grade_histories" (int) Include histories (default to 0 that is equal to no)' ), 'value' => new external_value(PARAM_RAW, 'the value for the option 1 (yes) or 0 (no)' ) ) ), 'Course duplication options', VALUE_DEFAULT, array() ), ) ); } /** * Duplicate a course * * @param int $courseid * @param string $fullname Duplicated course fullname * @param string $shortname Duplicated course shortname * @param int $categoryid Duplicated course parent category id * @param int $visible Duplicated course availability * @param array $options List of backup options * @return array New course info * @since Moodle 2.3 */ public static function duplicate_course($courseid, $fullname, $shortname, $categoryid, $visible = 1, $options = array()) { global $CFG, $USER, $DB; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); // Parameter validation. $params = self::validate_parameters( self::duplicate_course_parameters(), array( 'courseid' => $courseid, 'fullname' => $fullname, 'shortname' => $shortname, 'categoryid' => $categoryid, 'visible' => $visible, 'options' => $options ) ); // Context validation. if (! ($course = $DB->get_record('course', array('id'=>$params['courseid'])))) { throw new moodle_exception('invalidcourseid', 'error'); } // Category where duplicated course is going to be created. $categorycontext = context_coursecat::instance($params['categoryid']); self::validate_context($categorycontext); // Course to be duplicated. $coursecontext = context_course::instance($course->id); self::validate_context($coursecontext); $backupdefaults = array( 'activities' => 1, 'blocks' => 1, 'filters' => 1, 'users' => 0, 'enrolments' => backup::ENROL_WITHUSERS, 'role_assignments' => 0, 'comments' => 0, 'userscompletion' => 0, 'logs' => 0, 'grade_histories' => 0 ); $backupsettings = array(); // Check for backup and restore options. if (!empty($params['options'])) { foreach ($params['options'] as $option) { // Strict check for a correct value (allways 1 or 0, true or false). $value = clean_param($option['value'], PARAM_INT); if ($value !== 0 and $value !== 1) { throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']); } if (!isset($backupdefaults[$option['name']])) { throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']); } $backupsettings[$option['name']] = $value; } } // Capability checking. // The backup controller check for this currently, this may be redundant. require_capability('moodle/course:create', $categorycontext); require_capability('moodle/restore:restorecourse', $categorycontext); require_capability('moodle/backup:backupcourse', $coursecontext); if (!empty($backupsettings['users'])) { require_capability('moodle/backup:userinfo', $coursecontext); require_capability('moodle/restore:userinfo', $categorycontext); } // Check if the shortname is used. if ($foundcourses = $DB->get_records('course', array('shortname'=>$shortname))) { foreach ($foundcourses as $foundcourse) { $foundcoursenames[] = $foundcourse->fullname; } $foundcoursenamestring = implode(',', $foundcoursenames); throw new moodle_exception('shortnametaken', '', '', $foundcoursenamestring); } // Backup the course. $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id); foreach ($backupsettings as $name => $value) { if ($setting = $bc->get_plan()->get_setting($name)) { $bc->get_plan()->get_setting($name)->set_value($value); } } $backupid = $bc->get_backupid(); $backupbasepath = $bc->get_plan()->get_basepath(); $bc->execute_plan(); $results = $bc->get_results(); $file = $results['backup_destination']; $bc->destroy(); // Restore the backup immediately. // Check if we need to unzip the file because the backup temp dir does not contains backup files. if (!file_exists($backupbasepath . "/moodle_backup.xml")) { $file->extract_to_pathname(get_file_packer('application/vnd.moodle.backup'), $backupbasepath); } // Create new course. $newcourseid = restore_dbops::create_new_course($params['fullname'], $params['shortname'], $params['categoryid']); $rc = new restore_controller($backupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id, backup::TARGET_NEW_COURSE); foreach ($backupsettings as $name => $value) { $setting = $rc->get_plan()->get_setting($name); if ($setting->get_status() == backup_setting::NOT_LOCKED) { $setting->set_value($value); } } if (!$rc->execute_precheck()) { $precheckresults = $rc->get_precheck_results(); if (is_array($precheckresults) && !empty($precheckresults['errors'])) { if (empty($CFG->keeptempdirectoriesonbackup)) { fulldelete($backupbasepath); } $errorinfo = ''; foreach ($precheckresults['errors'] as $error) { $errorinfo .= $error; } if (array_key_exists('warnings', $precheckresults)) { foreach ($precheckresults['warnings'] as $warning) { $errorinfo .= $warning; } } throw new moodle_exception('backupprecheckerrors', 'webservice', '', $errorinfo); } } $rc->execute_plan(); $rc->destroy(); $course = $DB->get_record('course', array('id' => $newcourseid), '*', MUST_EXIST); $course->fullname = $params['fullname']; $course->shortname = $params['shortname']; $course->visible = $params['visible']; // Set shortname and fullname back. $DB->update_record('course', $course); if (empty($CFG->keeptempdirectoriesonbackup)) { fulldelete($backupbasepath); } // Delete the course backup file created by this WebService. Originally located in the course backups area. $file->delete(); return array('id' => $course->id, 'shortname' => $course->shortname); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.3 */ public static function duplicate_course_returns() { return new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'course id'), 'shortname' => new external_value(PARAM_RAW, 'short name'), ) ); } /** * Returns description of method parameters for import_course * * @return external_function_parameters * @since Moodle 2.4 */ public static function import_course_parameters() { return new external_function_parameters( array( 'importfrom' => new external_value(PARAM_INT, 'the id of the course we are importing from'), 'importto' => new external_value(PARAM_INT, 'the id of the course we are importing to'), 'deletecontent' => new external_value(PARAM_INT, 'whether to delete the course content where we are importing to (default to 0 = No)', VALUE_DEFAULT, 0), 'options' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_ALPHA, 'The backup option name: "activities" (int) Include course activites (default to 1 that is equal to yes), "blocks" (int) Include course blocks (default to 1 that is equal to yes), "filters" (int) Include course filters (default to 1 that is equal to yes)' ), 'value' => new external_value(PARAM_RAW, 'the value for the option 1 (yes) or 0 (no)' ) ) ), 'Course import options', VALUE_DEFAULT, array() ), ) ); } /** * Imports a course * * @param int $importfrom The id of the course we are importing from * @param int $importto The id of the course we are importing to * @param bool $deletecontent Whether to delete the course we are importing to content * @param array $options List of backup options * @return null * @since Moodle 2.4 */ public static function import_course($importfrom, $importto, $deletecontent = 0, $options = array()) { global $CFG, $USER, $DB; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); // Parameter validation. $params = self::validate_parameters( self::import_course_parameters(), array( 'importfrom' => $importfrom, 'importto' => $importto, 'deletecontent' => $deletecontent, 'options' => $options ) ); if ($params['deletecontent'] !== 0 and $params['deletecontent'] !== 1) { throw new moodle_exception('invalidextparam', 'webservice', '', $params['deletecontent']); } // Context validation. if (! ($importfrom = $DB->get_record('course', array('id'=>$params['importfrom'])))) { throw new moodle_exception('invalidcourseid', 'error'); } if (! ($importto = $DB->get_record('course', array('id'=>$params['importto'])))) { throw new moodle_exception('invalidcourseid', 'error'); } $importfromcontext = context_course::instance($importfrom->id); self::validate_context($importfromcontext); $importtocontext = context_course::instance($importto->id); self::validate_context($importtocontext); $backupdefaults = array( 'activities' => 1, 'blocks' => 1, 'filters' => 1 ); $backupsettings = array(); // Check for backup and restore options. if (!empty($params['options'])) { foreach ($params['options'] as $option) { // Strict check for a correct value (allways 1 or 0, true or false). $value = clean_param($option['value'], PARAM_INT); if ($value !== 0 and $value !== 1) { throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']); } if (!isset($backupdefaults[$option['name']])) { throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']); } $backupsettings[$option['name']] = $value; } } // Capability checking. require_capability('moodle/backup:backuptargetimport', $importfromcontext); require_capability('moodle/restore:restoretargetimport', $importtocontext); $bc = new backup_controller(backup::TYPE_1COURSE, $importfrom->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); foreach ($backupsettings as $name => $value) { $bc->get_plan()->get_setting($name)->set_value($value); } $backupid = $bc->get_backupid(); $backupbasepath = $bc->get_plan()->get_basepath(); $bc->execute_plan(); $bc->destroy(); // Restore the backup immediately. // Check if we must delete the contents of the destination course. if ($params['deletecontent']) { $restoretarget = backup::TARGET_EXISTING_DELETING; } else { $restoretarget = backup::TARGET_EXISTING_ADDING; } $rc = new restore_controller($backupid, $importto->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, $restoretarget); foreach ($backupsettings as $name => $value) { $rc->get_plan()->get_setting($name)->set_value($value); } if (!$rc->execute_precheck()) { $precheckresults = $rc->get_precheck_results(); if (is_array($precheckresults) && !empty($precheckresults['errors'])) { if (empty($CFG->keeptempdirectoriesonbackup)) { fulldelete($backupbasepath); } $errorinfo = ''; foreach ($precheckresults['errors'] as $error) { $errorinfo .= $error; } if (array_key_exists('warnings', $precheckresults)) { foreach ($precheckresults['warnings'] as $warning) { $errorinfo .= $warning; } } throw new moodle_exception('backupprecheckerrors', 'webservice', '', $errorinfo); } } else { if ($restoretarget == backup::TARGET_EXISTING_DELETING) { restore_dbops::delete_course_content($importto->id); } } $rc->execute_plan(); $rc->destroy(); if (empty($CFG->keeptempdirectoriesonbackup)) { fulldelete($backupbasepath); } return null; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.4 */ public static function import_course_returns() { return null; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function get_categories_parameters() { return new external_function_parameters( array( 'criteria' => new external_multiple_structure( new external_single_structure( array( 'key' => new external_value(PARAM_ALPHA, 'The category column to search, expected keys (value format) are:'. '"id" (int) the category id,'. '"ids" (string) category ids separated by commas,'. '"name" (string) the category name,'. '"parent" (int) the parent category id,'. '"idnumber" (string) category idnumber'. ' - user must have \'moodle/category:manage\' to search on idnumber,'. '"visible" (int) whether the returned categories must be visible or hidden. If the key is not passed, then the function return all categories that the user can see.'. ' - user must have \'moodle/category:manage\' or \'moodle/category:viewhiddencategories\' to search on visible,'. '"theme" (string) only return the categories having this theme'. ' - user must have \'moodle/category:manage\' to search on theme'), 'value' => new external_value(PARAM_RAW, 'the value to match') ) ), 'criteria', VALUE_DEFAULT, array() ), 'addsubcategories' => new external_value(PARAM_BOOL, 'return the sub categories infos (1 - default) otherwise only the category info (0)', VALUE_DEFAULT, 1) ) ); } /** * Get categories * * @param array $criteria Criteria to match the results * @param booln $addsubcategories obtain only the category (false) or its subcategories (true - default) * @return array list of categories * @since Moodle 2.3 */ public static function get_categories($criteria = array(), $addsubcategories = true) { global $CFG, $DB; require_once($CFG->dirroot . "/course/lib.php"); // Validate parameters. $params = self::validate_parameters(self::get_categories_parameters(), array('criteria' => $criteria, 'addsubcategories' => $addsubcategories)); // Retrieve the categories. $categories = array(); if (!empty($params['criteria'])) { $conditions = array(); $wheres = array(); foreach ($params['criteria'] as $crit) { $key = trim($crit['key']); // Trying to avoid duplicate keys. if (!isset($conditions[$key])) { $context = context_system::instance(); $value = null; switch ($key) { case 'id': $value = clean_param($crit['value'], PARAM_INT); $conditions[$key] = $value; $wheres[] = $key . " = :" . $key; break; case 'ids': $value = clean_param($crit['value'], PARAM_SEQUENCE); $ids = explode(',', $value); list($sqlids, $paramids) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); $conditions = array_merge($conditions, $paramids); $wheres[] = 'id ' . $sqlids; break; case 'idnumber': if (has_capability('moodle/category:manage', $context)) { $value = clean_param($crit['value'], PARAM_RAW); $conditions[$key] = $value; $wheres[] = $key . " = :" . $key; } else { // We must throw an exception. // Otherwise the dev client would think no idnumber exists. throw new moodle_exception('criteriaerror', 'webservice', '', null, 'You don\'t have the permissions to search on the "idnumber" field.'); } break; case 'name': $value = clean_param($crit['value'], PARAM_TEXT); $conditions[$key] = $value; $wheres[] = $key . " = :" . $key; break; case 'parent': $value = clean_param($crit['value'], PARAM_INT); $conditions[$key] = $value; $wheres[] = $key . " = :" . $key; break; case 'visible': if (has_capability('moodle/category:viewhiddencategories', $context)) { $value = clean_param($crit['value'], PARAM_INT); $conditions[$key] = $value; $wheres[] = $key . " = :" . $key; } else { throw new moodle_exception('criteriaerror', 'webservice', '', null, 'You don\'t have the permissions to search on the "visible" field.'); } break; case 'theme': if (has_capability('moodle/category:manage', $context)) { $value = clean_param($crit['value'], PARAM_THEME); $conditions[$key] = $value; $wheres[] = $key . " = :" . $key; } else { throw new moodle_exception('criteriaerror', 'webservice', '', null, 'You don\'t have the permissions to search on the "theme" field.'); } break; default: throw new moodle_exception('criteriaerror', 'webservice', '', null, 'You can not search on this criteria: ' . $key); } } } if (!empty($wheres)) { $wheres = implode(" AND ", $wheres); $categories = $DB->get_records_select('course_categories', $wheres, $conditions); // Retrieve its sub subcategories (all levels). if ($categories and !empty($params['addsubcategories'])) { $newcategories = array(); // Check if we required visible/theme checks. $additionalselect = ''; $additionalparams = array(); if (isset($conditions['visible'])) { $additionalselect .= ' AND visible = :visible'; $additionalparams['visible'] = $conditions['visible']; } if (isset($conditions['theme'])) { $additionalselect .= ' AND theme= :theme'; $additionalparams['theme'] = $conditions['theme']; } foreach ($categories as $category) { $sqlselect = $DB->sql_like('path', ':path') . $additionalselect; $sqlparams = array('path' => $category->path.'/%') + $additionalparams; // It will NOT include the specified category. $subcategories = $DB->get_records_select('course_categories', $sqlselect, $sqlparams); $newcategories = $newcategories + $subcategories; // Both arrays have integer as keys. } $categories = $categories + $newcategories; } } } else { // Retrieve all categories in the database. $categories = $DB->get_records('course_categories'); } // The not returned categories. key => category id, value => reason of exclusion. $excludedcats = array(); // The returned categories. $categoriesinfo = array(); // We need to sort the categories by path. // The parent cats need to be checked by the algo first. usort($categories, "core_course_external::compare_categories_by_path"); foreach ($categories as $category) { // Check if the category is a child of an excluded category, if yes exclude it too (excluded => do not return). $parents = explode('/', $category->path); unset($parents[0]); // First key is always empty because path start with / => /1/2/4. foreach ($parents as $parentid) { // Note: when the parent exclusion was due to the context, // the sub category could still be returned. if (isset($excludedcats[$parentid]) and $excludedcats[$parentid] != 'context') { $excludedcats[$category->id] = 'parent'; } } // Check the user can use the category context. $context = context_coursecat::instance($category->id); try { self::validate_context($context); } catch (Exception $e) { $excludedcats[$category->id] = 'context'; // If it was the requested category then throw an exception. if (isset($params['categoryid']) && $category->id == $params['categoryid']) { $exceptionparam = new stdClass(); $exceptionparam->message = $e->getMessage(); $exceptionparam->catid = $category->id; throw new moodle_exception('errorcatcontextnotvalid', 'webservice', '', $exceptionparam); } } // Return the category information. if (!isset($excludedcats[$category->id])) { // Final check to see if the category is visible to the user. if (core_course_category::can_view_category($category)) { $categoryinfo = array(); $categoryinfo['id'] = $category->id; $categoryinfo['name'] = \core_external\util::format_string($category->name, $context); list($categoryinfo['description'], $categoryinfo['descriptionformat']) = \core_external\util::format_text($category->description, $category->descriptionformat, $context, 'coursecat', 'description', null); $categoryinfo['parent'] = $category->parent; $categoryinfo['sortorder'] = $category->sortorder; $categoryinfo['coursecount'] = $category->coursecount; $categoryinfo['depth'] = $category->depth; $categoryinfo['path'] = $category->path; // Some fields only returned for admin. if (has_capability('moodle/category:manage', $context)) { $categoryinfo['idnumber'] = $category->idnumber; $categoryinfo['visible'] = $category->visible; $categoryinfo['visibleold'] = $category->visibleold; $categoryinfo['timemodified'] = $category->timemodified; $categoryinfo['theme'] = clean_param($category->theme, PARAM_THEME); } $categoriesinfo[] = $categoryinfo; } else { $excludedcats[$category->id] = 'visibility'; } } } // Sorting the resulting array so it looks a bit better for the client developer. usort($categoriesinfo, "core_course_external::compare_categories_by_sortorder"); return $categoriesinfo; } /** * Sort categories array by path * private function: only used by get_categories * * @param stdClass $category1 * @param stdClass $category2 * @return int result of strcmp * @since Moodle 2.3 */ private static function compare_categories_by_path($category1, $category2) { return strcmp($category1->path, $category2->path); } /** * Sort categories array by sortorder * private function: only used by get_categories * * @param array $category1 * @param array $category2 * @return int result of strcmp * @since Moodle 2.3 */ private static function compare_categories_by_sortorder($category1, $category2) { return strcmp($category1['sortorder'], $category2['sortorder']); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.3 */ public static function get_categories_returns() { return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'category id'), 'name' => new external_value(PARAM_RAW, 'category name'), 'idnumber' => new external_value(PARAM_RAW, 'category id number', VALUE_OPTIONAL), 'description' => new external_value(PARAM_RAW, 'category description'), 'descriptionformat' => new external_format_value('description'), 'parent' => new external_value(PARAM_INT, 'parent category id'), 'sortorder' => new external_value(PARAM_INT, 'category sorting order'), 'coursecount' => new external_value(PARAM_INT, 'number of courses in this category'), 'visible' => new external_value(PARAM_INT, '1: available, 0:not available', VALUE_OPTIONAL), 'visibleold' => new external_value(PARAM_INT, '1: available, 0:not available', VALUE_OPTIONAL), 'timemodified' => new external_value(PARAM_INT, 'timestamp', VALUE_OPTIONAL), 'depth' => new external_value(PARAM_INT, 'category depth'), 'path' => new external_value(PARAM_TEXT, 'category path'), 'theme' => new external_value(PARAM_THEME, 'category theme', VALUE_OPTIONAL), ), 'List of categories' ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function create_categories_parameters() { return new external_function_parameters( array( 'categories' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_TEXT, 'new category name'), 'parent' => new external_value(PARAM_INT, 'the parent category id inside which the new category will be created - set to 0 for a root category', VALUE_DEFAULT, 0), 'idnumber' => new external_value(PARAM_RAW, 'the new category idnumber', VALUE_OPTIONAL), 'description' => new external_value(PARAM_RAW, 'the new category description', VALUE_OPTIONAL), 'descriptionformat' => new external_format_value('description', VALUE_DEFAULT), 'theme' => new external_value(PARAM_THEME, 'the new category theme. This option must be enabled on moodle', VALUE_OPTIONAL), ) ) ) ) ); } /** * Create categories * * @param array $categories - see create_categories_parameters() for the array structure * @return array - see create_categories_returns() for the array structure * @since Moodle 2.3 */ public static function create_categories($categories) { global $DB; $params = self::validate_parameters(self::create_categories_parameters(), array('categories' => $categories)); $transaction = $DB->start_delegated_transaction(); $createdcategories = array(); foreach ($params['categories'] as $category) { if ($category['parent']) { if (!$DB->record_exists('course_categories', array('id' => $category['parent']))) { throw new moodle_exception('unknowcategory'); } $context = context_coursecat::instance($category['parent']); } else { $context = context_system::instance(); } self::validate_context($context); require_capability('moodle/category:manage', $context); // this will validate format and throw an exception if there are errors util::validate_format($category['descriptionformat']); $newcategory = core_course_category::create($category); $context = context_coursecat::instance($newcategory->id); $createdcategories[] = array( 'id' => $newcategory->id, 'name' => \core_external\util::format_string($newcategory->name, $context), ); } $transaction->allow_commit(); return $createdcategories; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function create_categories_returns() { return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'new category id'), 'name' => new external_value(PARAM_RAW, 'new category name'), ) ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function update_categories_parameters() { return new external_function_parameters( array( 'categories' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'course id'), 'name' => new external_value(PARAM_TEXT, 'category name', VALUE_OPTIONAL), 'idnumber' => new external_value(PARAM_RAW, 'category id number', VALUE_OPTIONAL), 'parent' => new external_value(PARAM_INT, 'parent category id', VALUE_OPTIONAL), 'description' => new external_value(PARAM_RAW, 'category description', VALUE_OPTIONAL), 'descriptionformat' => new external_format_value('description', VALUE_DEFAULT), 'theme' => new external_value(PARAM_THEME, 'the category theme. This option must be enabled on moodle', VALUE_OPTIONAL), ) ) ) ) ); } /** * Update categories * * @param array $categories The list of categories to update * @return null * @since Moodle 2.3 */ public static function update_categories($categories) { global $DB; // Validate parameters. $params = self::validate_parameters(self::update_categories_parameters(), array('categories' => $categories)); $transaction = $DB->start_delegated_transaction(); foreach ($params['categories'] as $cat) { $category = core_course_category::get($cat['id']); $categorycontext = context_coursecat::instance($cat['id']); self::validate_context($categorycontext); require_capability('moodle/category:manage', $categorycontext); // If the category parent is being changed, check for capability in the new parent category if (isset($cat['parent']) && ($cat['parent'] !== $category->parent)) { if ($cat['parent'] == 0) { // Creating a top level category requires capability in the system context $parentcontext = context_system::instance(); } else { // Category context $parentcontext = context_coursecat::instance($cat['parent']); } self::validate_context($parentcontext); require_capability('moodle/category:manage', $parentcontext); } // this will throw an exception if descriptionformat is not valid util::validate_format($cat['descriptionformat']); $category->update($cat); } $transaction->allow_commit(); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.3 */ public static function update_categories_returns() { return null; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function delete_categories_parameters() { return new external_function_parameters( array( 'categories' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'category id to delete'), 'newparent' => new external_value(PARAM_INT, 'the parent category to move the contents to, if specified', VALUE_OPTIONAL), 'recursive' => new external_value(PARAM_BOOL, '1: recursively delete all contents inside this category, 0 (default): move contents to newparent or current parent category (except if parent is root)', VALUE_DEFAULT, 0) ) ) ) ) ); } /** * Delete categories * * @param array $categories A list of category ids * @return array * @since Moodle 2.3 */ public static function delete_categories($categories) { global $CFG, $DB; require_once($CFG->dirroot . "/course/lib.php"); // Validate parameters. $params = self::validate_parameters(self::delete_categories_parameters(), array('categories' => $categories)); $transaction = $DB->start_delegated_transaction(); foreach ($params['categories'] as $category) { $deletecat = core_course_category::get($category['id'], MUST_EXIST); $context = context_coursecat::instance($deletecat->id); require_capability('moodle/category:manage', $context); self::validate_context($context); self::validate_context(get_category_or_system_context($deletecat->parent)); if ($category['recursive']) { // If recursive was specified, then we recursively delete the category's contents. if ($deletecat->can_delete_full()) { $deletecat->delete_full(false); } else { throw new moodle_exception('youcannotdeletecategory', '', '', $deletecat->get_formatted_name()); } } else { // In this situation, we don't delete the category's contents, we either move it to newparent or parent. // If the parent is the root, moving is not supported (because a course must always be inside a category). // We must move to an existing category. if (!empty($category['newparent'])) { $newparentcat = core_course_category::get($category['newparent']); } else { $newparentcat = core_course_category::get($deletecat->parent); } // This operation is not allowed. We must move contents to an existing category. if (!$newparentcat->id) { throw new moodle_exception('movecatcontentstoroot'); } self::validate_context(context_coursecat::instance($newparentcat->id)); if ($deletecat->can_move_content_to($newparentcat->id)) { $deletecat->delete_move($newparentcat->id, false); } else { throw new moodle_exception('youcannotdeletecategory', '', '', $deletecat->get_formatted_name()); } } } $transaction->allow_commit(); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.3 */ public static function delete_categories_returns() { return null; } /** * Describes the parameters for delete_modules. * * @return external_function_parameters * @since Moodle 2.5 */ public static function delete_modules_parameters() { return new external_function_parameters ( array( 'cmids' => new external_multiple_structure(new external_value(PARAM_INT, 'course module ID', VALUE_REQUIRED, '', NULL_NOT_ALLOWED), 'Array of course module IDs'), ) ); } /** * Deletes a list of provided module instances. * * @param array $cmids the course module ids * @since Moodle 2.5 */ public static function delete_modules($cmids) { global $CFG, $DB; // Require course file containing the course delete module function. require_once($CFG->dirroot . "/course/lib.php"); // Clean the parameters. $params = self::validate_parameters(self::delete_modules_parameters(), array('cmids' => $cmids)); // Keep track of the course ids we have performed a capability check on to avoid repeating. $arrcourseschecked = array(); foreach ($params['cmids'] as $cmid) { // Get the course module. $cm = $DB->get_record('course_modules', array('id' => $cmid), '*', MUST_EXIST); // Check if we have not yet confirmed they have permission in this course. if (!in_array($cm->course, $arrcourseschecked)) { // Ensure the current user has required permission in this course. $context = context_course::instance($cm->course); self::validate_context($context); // Add to the array. $arrcourseschecked[] = $cm->course; } // Ensure they can delete this module. $modcontext = context_module::instance($cm->id); require_capability('moodle/course:manageactivities', $modcontext); // Delete the module. course_delete_module($cm->id); } } /** * Describes the delete_modules return value. * * @return external_single_structure * @since Moodle 2.5 */ public static function delete_modules_returns() { return null; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.9 */ public static function view_course_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'id of the course'), 'sectionnumber' => new external_value(PARAM_INT, 'section number', VALUE_DEFAULT, 0) ) ); } /** * Trigger the course viewed event. * * @param int $courseid id of course * @param int $sectionnumber sectionnumber (0, 1, 2...) * @return array of warnings and status result * @since Moodle 2.9 * @throws moodle_exception */ public static function view_course($courseid, $sectionnumber = 0) { global $CFG; require_once($CFG->dirroot . "/course/lib.php"); $params = self::validate_parameters(self::view_course_parameters(), array( 'courseid' => $courseid, 'sectionnumber' => $sectionnumber )); $warnings = array(); $course = get_course($params['courseid']); $context = context_course::instance($course->id); self::validate_context($context); if (!empty($params['sectionnumber'])) { // Get section details and check it exists. $modinfo = get_fast_modinfo($course); $coursesection = $modinfo->get_section_info($params['sectionnumber'], MUST_EXIST); // Check user is allowed to see it. if (!$coursesection->uservisible) { require_capability('moodle/course:viewhiddensections', $context); } } course_view($context, $params['sectionnumber']); $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.9 */ public static function view_course_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.0 */ public static function search_courses_parameters() { return new external_function_parameters( array( 'criterianame' => new external_value(PARAM_ALPHA, 'criteria name (search, modulelist (only admins), blocklist (only admins), tagid)'), 'criteriavalue' => new external_value(PARAM_RAW, 'criteria value'), 'page' => new external_value(PARAM_INT, 'page number (0 based)', VALUE_DEFAULT, 0), 'perpage' => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0), 'requiredcapabilities' => new external_multiple_structure( new external_value(PARAM_CAPABILITY, 'Capability string used to filter courses by permission'), 'Optional list of required capabilities (used to filter the list)', VALUE_DEFAULT, array() ), 'limittoenrolled' => new external_value(PARAM_BOOL, 'limit to enrolled courses', VALUE_DEFAULT, 0), 'onlywithcompletion' => new external_value(PARAM_BOOL, 'limit to courses where completion is enabled', VALUE_DEFAULT, 0), ) ); } /** * Return the course information that is public (visible by every one) * * @param core_course_list_element $course course in list object * @param stdClass $coursecontext course context object * @return array the course information * @since Moodle 3.2 */ protected static function get_course_public_information(core_course_list_element $course, $coursecontext) { global $OUTPUT; static $categoriescache = array(); // Category information. if (!array_key_exists($course->category, $categoriescache)) { $categoriescache[$course->category] = core_course_category::get($course->category, IGNORE_MISSING); } $category = $categoriescache[$course->category]; // Retrieve course overview used files. $files = array(); foreach ($course->get_course_overviewfiles() as $file) { $fileurl = moodle_url::make_webservice_pluginfile_url($file->get_contextid(), $file->get_component(), $file->get_filearea(), null, $file->get_filepath(), $file->get_filename())->out(false); $files[] = array( 'filename' => $file->get_filename(), 'fileurl' => $fileurl, 'filesize' => $file->get_filesize(), 'filepath' => $file->get_filepath(), 'mimetype' => $file->get_mimetype(), 'timemodified' => $file->get_timemodified(), ); } // Retrieve the course contacts, // we need here the users fullname since if we are not enrolled can be difficult to obtain them via other Web Services. $coursecontacts = array(); foreach ($course->get_course_contacts() as $contact) { $coursecontacts[] = array( 'id' => $contact['user']->id, 'fullname' => $contact['username'], 'roles' => array_map(function($role){ return array('id' => $role->id, 'name' => $role->displayname); }, $contact['roles']), 'role' => array('id' => $contact['role']->id, 'name' => $contact['role']->displayname), 'rolename' => $contact['rolename'] ); } // Allowed enrolment methods (maybe we can self-enrol). $enroltypes = array(); $instances = enrol_get_instances($course->id, true); foreach ($instances as $instance) { $enroltypes[] = $instance->enrol; } // Format summary. list($summary, $summaryformat) = \core_external\util::format_text($course->summary, $course->summaryformat, $coursecontext, 'course', 'summary', null); $categoryname = ''; if (!empty($category)) { $categoryname = \core_external\util::format_string($category->name, $category->get_context()); } $displayname = get_course_display_name_for_list($course); $coursereturns = array(); $coursereturns['id'] = $course->id; $coursereturns['fullname'] = \core_external\util::format_string($course->fullname, $coursecontext); $coursereturns['displayname'] = \core_external\util::format_string($displayname, $coursecontext); $coursereturns['shortname'] = \core_external\util::format_string($course->shortname, $coursecontext); $coursereturns['categoryid'] = $course->category; $coursereturns['categoryname'] = $categoryname; $coursereturns['summary'] = $summary; $coursereturns['summaryformat'] = $summaryformat; $coursereturns['summaryfiles'] = util::get_area_files($coursecontext->id, 'course', 'summary', false, false); $coursereturns['overviewfiles'] = $files; $coursereturns['contacts'] = $coursecontacts; $coursereturns['enrollmentmethods'] = $enroltypes; $coursereturns['sortorder'] = $course->sortorder; $coursereturns['showactivitydates'] = $course->showactivitydates; $coursereturns['showcompletionconditions'] = $course->showcompletionconditions; $handler = core_course\customfield\course_handler::create(); if ($customfields = $handler->export_instance_data($course->id)) { $coursereturns['customfields'] = []; foreach ($customfields as $data) { $coursereturns['customfields'][] = [ 'type' => $data->get_type(), 'value' => $data->get_value(), 'valueraw' => $data->get_data_controller()->get_value(), 'name' => $data->get_name(), 'shortname' => $data->get_shortname() ]; } } $courseimage = \core_course\external\course_summary_exporter::get_course_image($course); if (!$courseimage) { $courseimage = $OUTPUT->get_generated_url_for_course($coursecontext); } $coursereturns['courseimage'] = $courseimage; return $coursereturns; } /** * Search courses following the specified criteria. * * @param string $criterianame Criteria name (search, modulelist (only admins), blocklist (only admins), tagid) * @param string $criteriavalue Criteria value * @param int $page Page number (for pagination) * @param int $perpage Items per page * @param array $requiredcapabilities Optional list of required capabilities (used to filter the list). * @param int $limittoenrolled Limit to only enrolled courses * @param int onlywithcompletion Limit to only courses where completion is enabled * @return array of course objects and warnings * @since Moodle 3.0 * @throws moodle_exception */ public static function search_courses($criterianame, $criteriavalue, $page=0, $perpage=0, $requiredcapabilities=array(), $limittoenrolled=0, $onlywithcompletion=0) { global $CFG; $warnings = array(); $parameters = array( 'criterianame' => $criterianame, 'criteriavalue' => $criteriavalue, 'page' => $page, 'perpage' => $perpage, 'requiredcapabilities' => $requiredcapabilities, 'limittoenrolled' => $limittoenrolled, 'onlywithcompletion' => $onlywithcompletion ); $params = self::validate_parameters(self::search_courses_parameters(), $parameters); self::validate_context(context_system::instance()); $allowedcriterianames = array('search', 'modulelist', 'blocklist', 'tagid'); if (!in_array($params['criterianame'], $allowedcriterianames)) { throw new invalid_parameter_exception('Invalid value for criterianame parameter (value: '.$params['criterianame'].'),' . 'allowed values are: '.implode(',', $allowedcriterianames)); } if ($params['criterianame'] == 'modulelist' or $params['criterianame'] == 'blocklist') { require_capability('moodle/site:config', context_system::instance()); } $paramtype = array( 'search' => PARAM_RAW, 'modulelist' => PARAM_PLUGIN, 'blocklist' => PARAM_INT, 'tagid' => PARAM_INT ); $params['criteriavalue'] = clean_param($params['criteriavalue'], $paramtype[$params['criterianame']]); // Prepare the search API options. $searchcriteria = array(); $searchcriteria[$params['criterianame']] = $params['criteriavalue']; if ($params['onlywithcompletion']) { $searchcriteria['onlywithcompletion'] = true; } if ($params['limittoenrolled']) { $searchcriteria['limittoenrolled'] = true; } $options = array(); if ($params['perpage'] != 0) { $offset = $params['page'] * $params['perpage']; $options = array('offset' => $offset, 'limit' => $params['perpage']); } // Search the courses. $courses = core_course_category::search_courses($searchcriteria, $options, $params['requiredcapabilities']); $totalcount = core_course_category::search_courses_count($searchcriteria, $options, $params['requiredcapabilities']); $finalcourses = array(); $categoriescache = array(); foreach ($courses as $course) { $coursecontext = context_course::instance($course->id); $finalcourses[] = self::get_course_public_information($course, $coursecontext); } return array( 'total' => $totalcount, 'courses' => $finalcourses, 'warnings' => $warnings ); } /** * Returns a course structure definition * * @param boolean $onlypublicdata set to true, to retrieve only fields viewable by anyone when the course is visible * @return external_single_structure the course structure * @since Moodle 3.2 */ protected static function get_course_structure($onlypublicdata = true) { $coursestructure = array( 'id' => new external_value(PARAM_INT, 'course id'), 'fullname' => new external_value(PARAM_RAW, 'course full name'), 'displayname' => new external_value(PARAM_RAW, 'course display name'), 'shortname' => new external_value(PARAM_RAW, 'course short name'), 'courseimage' => new external_value(PARAM_URL, 'Course image', VALUE_OPTIONAL), 'categoryid' => new external_value(PARAM_INT, 'category id'), 'categoryname' => new external_value(PARAM_RAW, 'category name'), 'sortorder' => new external_value(PARAM_INT, 'Sort order in the category', VALUE_OPTIONAL), 'summary' => new external_value(PARAM_RAW, 'summary'), 'summaryformat' => new external_format_value('summary'), 'summaryfiles' => new external_files('summary files in the summary field', VALUE_OPTIONAL), 'overviewfiles' => new external_files('additional overview files attached to this course'), 'showactivitydates' => new external_value(PARAM_BOOL, 'Whether the activity dates are shown or not'), 'showcompletionconditions' => new external_value(PARAM_BOOL, 'Whether the activity completion conditions are shown or not'), 'contacts' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'contact user id'), 'fullname' => new external_value(PARAM_NOTAGS, 'contact user fullname'), ) ), 'contact users' ), 'enrollmentmethods' => new external_multiple_structure( new external_value(PARAM_PLUGIN, 'enrollment method'), 'enrollment methods list' ), 'customfields' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_RAW, 'The name of the custom field'), 'shortname' => new external_value(PARAM_RAW, 'The shortname of the custom field - to be able to build the field class in the code'), 'type' => new external_value(PARAM_ALPHANUMEXT, 'The type of the custom field - text field, checkbox...'), 'valueraw' => new external_value(PARAM_RAW, 'The raw value of the custom field'), 'value' => new external_value(PARAM_RAW, 'The value of the custom field'), ) ), 'Custom fields', VALUE_OPTIONAL), ); if (!$onlypublicdata) { $extra = array( 'idnumber' => new external_value(PARAM_RAW, 'Id number', VALUE_OPTIONAL), 'format' => new external_value(PARAM_PLUGIN, 'Course format: weeks, topics, social, site,..', VALUE_OPTIONAL), 'showgrades' => new external_value(PARAM_INT, '1 if grades are shown, otherwise 0', VALUE_OPTIONAL), 'newsitems' => new external_value(PARAM_INT, 'Number of recent items appearing on the course page', VALUE_OPTIONAL), 'startdate' => new external_value(PARAM_INT, 'Timestamp when the course start', VALUE_OPTIONAL), 'enddate' => new external_value(PARAM_INT, 'Timestamp when the course end', VALUE_OPTIONAL), 'maxbytes' => new external_value(PARAM_INT, 'Largest size of file that can be uploaded into', VALUE_OPTIONAL), 'showreports' => new external_value(PARAM_INT, 'Are activity report shown (yes = 1, no =0)', VALUE_OPTIONAL), 'visible' => new external_value(PARAM_INT, '1: available to student, 0:not available', VALUE_OPTIONAL), 'groupmode' => new external_value(PARAM_INT, 'no group, separate, visible', VALUE_OPTIONAL), 'groupmodeforce' => new external_value(PARAM_INT, '1: yes, 0: no', VALUE_OPTIONAL), 'defaultgroupingid' => new external_value(PARAM_INT, 'default grouping id', VALUE_OPTIONAL), 'enablecompletion' => new external_value(PARAM_INT, 'Completion enabled? 1: yes 0: no', VALUE_OPTIONAL), 'completionnotify' => new external_value(PARAM_INT, '1: yes 0: no', VALUE_OPTIONAL), 'lang' => new external_value(PARAM_SAFEDIR, 'Forced course language', VALUE_OPTIONAL), 'theme' => new external_value(PARAM_PLUGIN, 'Fame of the forced theme', VALUE_OPTIONAL), 'marker' => new external_value(PARAM_INT, 'Current course marker', VALUE_OPTIONAL), 'legacyfiles' => new external_value(PARAM_INT, 'If legacy files are enabled', VALUE_OPTIONAL), 'calendartype' => new external_value(PARAM_PLUGIN, 'Calendar type', VALUE_OPTIONAL), 'timecreated' => new external_value(PARAM_INT, 'Time when the course was created', VALUE_OPTIONAL), 'timemodified' => new external_value(PARAM_INT, 'Last time the course was updated', VALUE_OPTIONAL), 'requested' => new external_value(PARAM_INT, 'If is a requested course', VALUE_OPTIONAL), 'cacherev' => new external_value(PARAM_INT, 'Cache revision number', VALUE_OPTIONAL), 'filters' => new external_multiple_structure( new external_single_structure( array( 'filter' => new external_value(PARAM_PLUGIN, 'Filter plugin name'), 'localstate' => new external_value(PARAM_INT, 'Filter state: 1 for on, -1 for off, 0 if inherit'), 'inheritedstate' => new external_value(PARAM_INT, '1 or 0 to use when localstate is set to inherit'), ) ), 'Course filters', VALUE_OPTIONAL ), 'courseformatoptions' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_RAW, 'Course format option name.'), 'value' => new external_value(PARAM_RAW, 'Course format option value.'), ) ), 'Additional options for particular course format.', VALUE_OPTIONAL ), 'communicationroomname' => new external_value(PARAM_TEXT, 'Communication tool room name.', VALUE_OPTIONAL), 'communicationroomurl' => new external_value(PARAM_RAW, 'Communication tool room URL.', VALUE_OPTIONAL), ); $coursestructure = array_merge($coursestructure, $extra); } return new external_single_structure($coursestructure); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.0 */ public static function search_courses_returns() { return new external_single_structure( array( 'total' => new external_value(PARAM_INT, 'total course count'), 'courses' => new external_multiple_structure(self::get_course_structure(), 'course'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.0 */ public static function get_course_module_parameters() { return new external_function_parameters( array( 'cmid' => new external_value(PARAM_INT, 'The course module id') ) ); } /** * Return information about a course module. * * @param int $cmid the course module id * @return array of warnings and the course module * @since Moodle 3.0 * @throws moodle_exception */ public static function get_course_module($cmid) { global $CFG, $DB; $params = self::validate_parameters(self::get_course_module_parameters(), array('cmid' => $cmid)); $warnings = array(); $cm = get_coursemodule_from_id(null, $params['cmid'], 0, true, MUST_EXIST); $context = context_module::instance($cm->id); self::validate_context($context); // If the user has permissions to manage the activity, return all the information. if (has_capability('moodle/course:manageactivities', $context)) { require_once($CFG->dirroot . '/course/modlib.php'); require_once($CFG->libdir . '/gradelib.php'); $info = $cm; // Get the extra information: grade, advanced grading and outcomes data. $course = get_course($cm->course); list($newcm, $newcontext, $module, $extrainfo, $cw) = get_moduleinfo_data($cm, $course); // Grades. $gradeinfo = array('grade', 'gradepass', 'gradecat'); foreach ($gradeinfo as $gfield) { if (isset($extrainfo->{$gfield})) { $info->{$gfield} = $extrainfo->{$gfield}; } } if (isset($extrainfo->grade) and $extrainfo->grade < 0) { $info->scale = $DB->get_field('scale', 'scale', array('id' => abs($extrainfo->grade))); } // Advanced grading. if (isset($extrainfo->_advancedgradingdata)) { $info->advancedgrading = array(); foreach ($extrainfo as $key => $val) { if (strpos($key, 'advancedgradingmethod_') === 0) { $info->advancedgrading[] = array( 'area' => str_replace('advancedgradingmethod_', '', $key), 'method' => $val ); } } } // Outcomes. foreach ($extrainfo as $key => $val) { if (strpos($key, 'outcome_') === 0) { if (!isset($info->outcomes)) { $info->outcomes = array(); } $id = str_replace('outcome_', '', $key); $outcome = grade_outcome::fetch(array('id' => $id)); $scaleitems = $outcome->load_scale(); $info->outcomes[] = array( 'id' => $id, 'name' => \core_external\util::format_string($outcome->get_name(), $context), 'scale' => $scaleitems->scale ); } } } else { // Return information is safe to show to any user. $info = new stdClass(); $info->id = $cm->id; $info->course = $cm->course; $info->module = $cm->module; $info->modname = $cm->modname; $info->instance = $cm->instance; $info->section = $cm->section; $info->sectionnum = $cm->sectionnum; $info->groupmode = $cm->groupmode; $info->groupingid = $cm->groupingid; $info->completion = $cm->completion; $info->downloadcontent = $cm->downloadcontent; } // Format name. $info->name = \core_external\util::format_string($cm->name, $context); $result = array(); $result['cm'] = $info; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.0 */ public static function get_course_module_returns() { return new external_single_structure( array( 'cm' => new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'The course module id'), 'course' => new external_value(PARAM_INT, 'The course id'), 'module' => new external_value(PARAM_INT, 'The module type id'), 'name' => new external_value(PARAM_RAW, 'The activity name'), 'modname' => new external_value(PARAM_COMPONENT, 'The module component name (forum, assign, etc..)'), 'instance' => new external_value(PARAM_INT, 'The activity instance id'), 'section' => new external_value(PARAM_INT, 'The module section id'), 'sectionnum' => new external_value(PARAM_INT, 'The module section number'), 'groupmode' => new external_value(PARAM_INT, 'Group mode'), 'groupingid' => new external_value(PARAM_INT, 'Grouping id'), 'completion' => new external_value(PARAM_INT, 'If completion is enabled'), 'idnumber' => new external_value(PARAM_RAW, 'Module id number', VALUE_OPTIONAL), 'added' => new external_value(PARAM_INT, 'Time added', VALUE_OPTIONAL), 'score' => new external_value(PARAM_INT, 'Score', VALUE_OPTIONAL), 'indent' => new external_value(PARAM_INT, 'Indentation', VALUE_OPTIONAL), 'visible' => new external_value(PARAM_INT, 'If visible', VALUE_OPTIONAL), 'visibleoncoursepage' => new external_value(PARAM_INT, 'If visible on course page', VALUE_OPTIONAL), 'visibleold' => new external_value(PARAM_INT, 'Visible old', VALUE_OPTIONAL), 'completiongradeitemnumber' => new external_value(PARAM_INT, 'Completion grade item', VALUE_OPTIONAL), 'completionpassgrade' => new external_value(PARAM_INT, 'Completion pass grade setting', VALUE_OPTIONAL), 'completionview' => new external_value(PARAM_INT, 'Completion view setting', VALUE_OPTIONAL), 'completionexpected' => new external_value(PARAM_INT, 'Completion time expected', VALUE_OPTIONAL), 'showdescription' => new external_value(PARAM_INT, 'If the description is showed', VALUE_OPTIONAL), 'downloadcontent' => new external_value(PARAM_INT, 'The download content value', VALUE_OPTIONAL), 'availability' => new external_value(PARAM_RAW, 'Availability settings', VALUE_OPTIONAL), 'grade' => new external_value(PARAM_FLOAT, 'Grade (max value or scale id)', VALUE_OPTIONAL), 'scale' => new external_value(PARAM_TEXT, 'Scale items (if used)', VALUE_OPTIONAL), 'gradepass' => new external_value(PARAM_RAW, 'Grade to pass (float)', VALUE_OPTIONAL), 'gradecat' => new external_value(PARAM_INT, 'Grade category', VALUE_OPTIONAL), 'advancedgrading' => new external_multiple_structure( new external_single_structure( array( 'area' => new external_value(PARAM_AREA, 'Gradable area name'), 'method' => new external_value(PARAM_COMPONENT, 'Grading method'), ) ), 'Advanced grading settings', VALUE_OPTIONAL ), 'outcomes' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_ALPHANUMEXT, 'Outcome id'), 'name' => new external_value(PARAM_RAW, 'Outcome full name'), 'scale' => new external_value(PARAM_TEXT, 'Scale items') ) ), 'Outcomes information', VALUE_OPTIONAL ), ) ), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.0 */ public static function get_course_module_by_instance_parameters() { return new external_function_parameters( array( 'module' => new external_value(PARAM_COMPONENT, 'The module name'), 'instance' => new external_value(PARAM_INT, 'The module instance id') ) ); } /** * Return information about a course module. * * @param string $module the module name * @param int $instance the activity instance id * @return array of warnings and the course module * @since Moodle 3.0 * @throws moodle_exception */ public static function get_course_module_by_instance($module, $instance) { $params = self::validate_parameters(self::get_course_module_by_instance_parameters(), array( 'module' => $module, 'instance' => $instance, )); $warnings = array(); $cm = get_coursemodule_from_instance($params['module'], $params['instance'], 0, false, MUST_EXIST); return self::get_course_module($cm->id); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.0 */ public static function get_course_module_by_instance_returns() { return self::get_course_module_returns(); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.2 */ public static function get_user_navigation_options_parameters() { return new external_function_parameters( array( 'courseids' => new external_multiple_structure(new external_value(PARAM_INT, 'Course id.')), ) ); } /** * Return a list of navigation options in a set of courses that are avaialable or not for the current user. * * @param array $courseids a list of course ids * @return array of warnings and the options availability * @since Moodle 3.2 * @throws moodle_exception */ public static function get_user_navigation_options($courseids) { global $CFG; require_once($CFG->dirroot . '/course/lib.php'); // Parameter validation. $params = self::validate_parameters(self::get_user_navigation_options_parameters(), array('courseids' => $courseids)); $courseoptions = array(); list($courses, $warnings) = util::validate_courses($params['courseids'], array(), true); if (!empty($courses)) { foreach ($courses as $course) { // Fix the context for the frontpage. if ($course->id == SITEID) { $course->context = context_system::instance(); } $navoptions = course_get_user_navigation_options($course->context, $course); $options = array(); foreach ($navoptions as $name => $available) { $options[] = array( 'name' => $name, 'available' => $available, ); } $courseoptions[] = array( 'id' => $course->id, 'options' => $options ); } } $result = array( 'courses' => $courseoptions, 'warnings' => $warnings ); return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.2 */ public static function get_user_navigation_options_returns() { return new external_single_structure( array( 'courses' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'Course id'), 'options' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_ALPHANUMEXT, 'Option name'), 'available' => new external_value(PARAM_BOOL, 'Whether the option is available or not'), ) ) ) ) ), 'List of courses' ), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.2 */ public static function get_user_administration_options_parameters() { return new external_function_parameters( array( 'courseids' => new external_multiple_structure(new external_value(PARAM_INT, 'Course id.')), ) ); } /** * Return a list of administration options in a set of courses that are available or not for the current user. * * @param array $courseids a list of course ids * @return array of warnings and the options availability * @since Moodle 3.2 * @throws moodle_exception */ public static function get_user_administration_options($courseids) { global $CFG; require_once($CFG->dirroot . '/course/lib.php'); // Parameter validation. $params = self::validate_parameters(self::get_user_administration_options_parameters(), array('courseids' => $courseids)); $courseoptions = array(); list($courses, $warnings) = util::validate_courses($params['courseids'], array(), true); if (!empty($courses)) { foreach ($courses as $course) { $adminoptions = course_get_user_administration_options($course, $course->context); $options = array(); foreach ($adminoptions as $name => $available) { $options[] = array( 'name' => $name, 'available' => $available, ); } $courseoptions[] = array( 'id' => $course->id, 'options' => $options ); } } $result = array( 'courses' => $courseoptions, 'warnings' => $warnings ); return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.2 */ public static function get_user_administration_options_returns() { return self::get_user_navigation_options_returns(); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.2 */ public static function get_courses_by_field_parameters() { return new external_function_parameters( array( 'field' => new external_value(PARAM_ALPHA, 'The field to search can be left empty for all courses or: id: course id ids: comma separated course ids shortname: course short name idnumber: course id number category: category id the course belongs to sectionid: section id that belongs to a course ', VALUE_DEFAULT, ''), 'value' => new external_value(PARAM_RAW, 'The value to match', VALUE_DEFAULT, '') ) ); } /** * Get courses matching a specific field (id/s, shortname, idnumber, category) * * @param string $field field name to search, or empty for all courses * @param string $value value to search * @return array list of courses and warnings * @throws invalid_parameter_exception * @since Moodle 3.2 */ public static function get_courses_by_field($field = '', $value = '') { global $DB, $CFG; require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->libdir . '/filterlib.php'); $params = self::validate_parameters(self::get_courses_by_field_parameters(), array( 'field' => $field, 'value' => $value, ) ); $warnings = array(); if (empty($params['field'])) { $courses = $DB->get_records('course', null, 'id ASC'); } else { switch ($params['field']) { case 'id': case 'category': case 'sectionid': $value = clean_param($params['value'], PARAM_INT); break; case 'ids': $value = clean_param($params['value'], PARAM_SEQUENCE); break; case 'shortname': $value = clean_param($params['value'], PARAM_TEXT); break; case 'idnumber': $value = clean_param($params['value'], PARAM_RAW); break; default: throw new invalid_parameter_exception('Invalid field name'); } if ($params['field'] === 'ids') { // Preload categories to avoid loading one at a time. $courseids = explode(',', $value); list ($listsql, $listparams) = $DB->get_in_or_equal($courseids); $categoryids = $DB->get_fieldset_sql(" SELECT DISTINCT cc.id FROM {course} c JOIN {course_categories} cc ON cc.id = c.category WHERE c.id $listsql", $listparams); core_course_category::get_many($categoryids); // Load and validate all courses. This is called because it loads the courses // more efficiently. list ($courses, $warnings) = util::validate_courses($courseids, [], false, true); } else if ($params['field'] === 'sectionid') { $courseid = $DB->get_field('course_sections', 'course', ['id' => $value]); $courses = $courseid ? [$DB->get_record('course', ['id' => $courseid])] : []; } else { $courses = $DB->get_records('course', array($params['field'] => $value), 'id ASC'); } } $iscommapiavailable = \core_communication\api::is_available(); $coursesdata = array(); foreach ($courses as $course) { $context = context_course::instance($course->id); $canupdatecourse = has_capability('moodle/course:update', $context); $canviewhiddencourses = has_capability('moodle/course:viewhiddencourses', $context); // Check if the course is visible in the site for the user. if (!$course->visible and !$canviewhiddencourses and !$canupdatecourse) { continue; } // Get the public course information, even if we are not enrolled. $courseinlist = new core_course_list_element($course); // Now, check if we have access to the course, unless it was already checked. try { if (empty($course->contextvalidated)) { self::validate_context($context); } } catch (Exception $e) { // User can not access the course, check if they can see the public information about the course and return it. if (core_course_category::can_view_course_info($course)) { $coursesdata[$course->id] = self::get_course_public_information($courseinlist, $context); } continue; } $coursesdata[$course->id] = self::get_course_public_information($courseinlist, $context); // Return information for any user that can access the course. $coursefields = array('format', 'showgrades', 'newsitems', 'startdate', 'enddate', 'maxbytes', 'showreports', 'visible', 'groupmode', 'groupmodeforce', 'defaultgroupingid', 'enablecompletion', 'completionnotify', 'lang', 'theme', 'marker'); // Course filters. $coursesdata[$course->id]['filters'] = filter_get_available_in_context($context); // Information for managers only. if ($canupdatecourse) { $managerfields = array('idnumber', 'legacyfiles', 'calendartype', 'timecreated', 'timemodified', 'requested', 'cacherev'); $coursefields = array_merge($coursefields, $managerfields); } // Populate fields. foreach ($coursefields as $field) { $coursesdata[$course->id][$field] = $course->{$field}; } // Clean lang and auth fields for external functions (it may content uninstalled themes or language packs). if (isset($coursesdata[$course->id]['theme'])) { $coursesdata[$course->id]['theme'] = clean_param($coursesdata[$course->id]['theme'], PARAM_THEME); } if (isset($coursesdata[$course->id]['lang'])) { $coursesdata[$course->id]['lang'] = clean_param($coursesdata[$course->id]['lang'], PARAM_LANG); } $courseformatoptions = course_get_format($course)->get_config_for_external(); foreach ($courseformatoptions as $key => $value) { $coursesdata[$course->id]['courseformatoptions'][] = array( 'name' => $key, 'value' => $value ); } // Communication tools for the course. if ($iscommapiavailable) { $communication = \core_communication\api::load_by_instance( context: $context, component: 'core_course', instancetype: 'coursecommunication', instanceid: $course->id ); if ($communication->get_provider()) { $coursesdata[$course->id]['communicationroomname'] = \core_external\util::format_string($communication->get_room_name(), $context); // This will be usually an URL, however, it is better to consider that can be anything a plugin might return, this is why we will use PARAM_RAW. $coursesdata[$course->id]['communicationroomurl'] = $communication->get_communication_room_url(); } } } return array( 'courses' => $coursesdata, 'warnings' => $warnings ); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.2 */ public static function get_courses_by_field_returns() { // Course structure, including not only public viewable fields. return new external_single_structure( array( 'courses' => new external_multiple_structure(self::get_course_structure(false), 'Course'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.2 */ public static function check_updates_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'Course id to check'), 'tocheck' => new external_multiple_structure( new external_single_structure( array( 'contextlevel' => new external_value(PARAM_ALPHA, 'The context level for the file location. Only module supported right now.'), 'id' => new external_value(PARAM_INT, 'Context instance id'), 'since' => new external_value(PARAM_INT, 'Check updates since this time stamp'), ) ), 'Instances to check' ), 'filter' => new external_multiple_structure( new external_value(PARAM_ALPHANUM, 'Area name: configuration, fileareas, completion, ratings, comments, gradeitems, outcomes'), 'Check only for updates in these areas', VALUE_DEFAULT, array() ) ) ); } /** * Check if there is updates affecting the user for the given course and contexts. * Right now only modules are supported. * This WS calls mod_check_updates_since for each module to check if there is any update the user should we aware of. * * @param int $courseid the list of modules to check * @param array $tocheck the list of modules to check * @param array $filter check only for updates in these areas * @return array list of updates and warnings * @throws moodle_exception * @since Moodle 3.2 */ public static function check_updates($courseid, $tocheck, $filter = array()) { global $CFG, $DB; require_once($CFG->dirroot . "/course/lib.php"); $params = self::validate_parameters( self::check_updates_parameters(), array( 'courseid' => $courseid, 'tocheck' => $tocheck, 'filter' => $filter, ) ); $course = get_course($params['courseid']); $context = context_course::instance($course->id); self::validate_context($context); list($instances, $warnings) = course_check_updates($course, $params['tocheck'], $filter); $instancesformatted = array(); foreach ($instances as $instance) { $updates = array(); foreach ($instance['updates'] as $name => $data) { if (empty($data->updated)) { continue; } $updatedata = array( 'name' => $name, ); if (!empty($data->timeupdated)) { $updatedata['timeupdated'] = $data->timeupdated; } if (!empty($data->itemids)) { $updatedata['itemids'] = $data->itemids; } $updates[] = $updatedata; } if (!empty($updates)) { $instancesformatted[] = array( 'contextlevel' => $instance['contextlevel'], 'id' => $instance['id'], 'updates' => $updates ); } } return array( 'instances' => $instancesformatted, 'warnings' => $warnings ); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.2 */ public static function check_updates_returns() { return new external_single_structure( array( 'instances' => new external_multiple_structure( new external_single_structure( array( 'contextlevel' => new external_value(PARAM_ALPHA, 'The context level'), 'id' => new external_value(PARAM_INT, 'Instance id'), 'updates' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_ALPHANUMEXT, 'Name of the area updated.'), 'timeupdated' => new external_value(PARAM_INT, 'Last time was updated', VALUE_OPTIONAL), 'itemids' => new external_multiple_structure( new external_value(PARAM_INT, 'Instance id'), 'The ids of the items updated', VALUE_OPTIONAL ) ) ) ) ) ) ), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_updates_since_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'Course id to check'), 'since' => new external_value(PARAM_INT, 'Check updates since this time stamp'), 'filter' => new external_multiple_structure( new external_value(PARAM_ALPHANUM, 'Area name: configuration, fileareas, completion, ratings, comments, gradeitems, outcomes'), 'Check only for updates in these areas', VALUE_DEFAULT, array() ) ) ); } /** * Check if there are updates affecting the user for the given course since the given time stamp. * * This function is a wrapper of self::check_updates for retrieving all the updates since a given time for all the activities. * * @param int $courseid the list of modules to check * @param int $since check updates since this time stamp * @param array $filter check only for updates in these areas * @return array list of updates and warnings * @throws moodle_exception * @since Moodle 3.3 */ public static function get_updates_since($courseid, $since, $filter = array()) { global $CFG, $DB; $params = self::validate_parameters( self::get_updates_since_parameters(), array( 'courseid' => $courseid, 'since' => $since, 'filter' => $filter, ) ); $course = get_course($params['courseid']); $modinfo = get_fast_modinfo($course); $tocheck = array(); // Retrieve all the visible course modules for the current user. $cms = $modinfo->get_cms(); foreach ($cms as $cm) { if (!$cm->uservisible) { continue; } $tocheck[] = array( 'id' => $cm->id, 'contextlevel' => 'module', 'since' => $params['since'], ); } return self::check_updates($course->id, $tocheck, $params['filter']); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.3 */ public static function get_updates_since_returns() { return self::check_updates_returns(); } /** * Mark the edit_module as deprecated. * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return bool */ public static function edit_module_is_deprecated() { return true; } /** * Parameters for function edit_module() * * @deprecated since 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return external_function_parameters */ public static function edit_module_parameters() { return new external_function_parameters( array( 'action' => new external_value(PARAM_ALPHA, 'action: hide, show, stealth, duplicate, delete, moveleft, moveright, group...', VALUE_REQUIRED), 'id' => new external_value(PARAM_INT, 'course module id', VALUE_REQUIRED), 'sectionreturn' => new external_value(PARAM_INT, 'section to return to', VALUE_DEFAULT, null), )); } /** * Performs one of the edit module actions and return new html for AJAX * * Returns html to replace the current module html with, for example: * - empty string for "delete" action, * - two modules html for "duplicate" action * - updated module html for everything else * * Throws exception if operation is not permitted/possible * * @deprecated since 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @param string $action * @param int $id * @param null|int $sectionreturn * @return string */ public static function edit_module($action, $id, $sectionreturn = null) { global $PAGE, $DB; // Validate and normalize parameters. $params = self::validate_parameters(self::edit_module_parameters(), array('action' => $action, 'id' => $id, 'sectionreturn' => $sectionreturn)); $action = $params['action']; $id = $params['id']; $sectionreturn = $params['sectionreturn']; // Set of permissions an editing user may have. $contextarray = [ 'moodle/course:update', 'moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/course:sectionvisibility', 'moodle/course:movesections', 'moodle/course:setcurrentsection', ]; $PAGE->set_other_editing_capability($contextarray); list($course, $cm) = get_course_and_cm_from_cmid($id); $modcontext = context_module::instance($cm->id); $coursecontext = context_course::instance($course->id); self::validate_context($modcontext); $format = course_get_format($course); if (!is_null($sectionreturn)) { $format->set_sectionnum($sectionreturn); } $renderer = $format->get_renderer($PAGE); switch($action) { case 'hide': case 'show': case 'stealth': require_capability('moodle/course:activityvisibility', $modcontext); $visible = ($action === 'hide') ? 0 : 1; $visibleoncoursepage = ($action === 'stealth') ? 0 : 1; set_coursemodule_visible($id, $visible, $visibleoncoursepage); \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger(); break; case 'duplicate': require_capability('moodle/course:manageactivities', $coursecontext); require_capability('moodle/backup:backuptargetimport', $coursecontext); require_capability('moodle/restore:restoretargetimport', $coursecontext); if (!course_allowed_module($course, $cm->modname)) { throw new moodle_exception('No permission to create that activity'); } if ($newcm = duplicate_module($course, $cm)) { $modinfo = $format->get_modinfo(); $section = $modinfo->get_section_info($newcm->sectionnum); $cm = $modinfo->get_cm($id); // Get both original and new element html. $result = $renderer->course_section_updated_cm_item($format, $section, $cm); $result .= $renderer->course_section_updated_cm_item($format, $section, $newcm); return $result; } break; case 'groupsseparate': case 'groupsvisible': case 'groupsnone': require_capability('moodle/course:manageactivities', $modcontext); if ($action === 'groupsseparate') { $newgroupmode = SEPARATEGROUPS; } else if ($action === 'groupsvisible') { $newgroupmode = VISIBLEGROUPS; } else { $newgroupmode = NOGROUPS; } if (set_coursemodule_groupmode($cm->id, $newgroupmode)) { \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger(); } break; case 'moveleft': case 'moveright': require_capability('moodle/course:manageactivities', $modcontext); $indent = $cm->indent + (($action === 'moveright') ? 1 : -1); if ($cm->indent >= 0) { $DB->update_record('course_modules', array('id' => $cm->id, 'indent' => $indent)); rebuild_course_cache($cm->course); } break; case 'delete': require_capability('moodle/course:manageactivities', $modcontext); course_delete_module($cm->id, true); return ''; default: throw new coding_exception('Unrecognised action'); } $modinfo = $format->get_modinfo(); $section = $modinfo->get_section_info($cm->sectionnum); $cm = $modinfo->get_cm($id); return $renderer->course_section_updated_cm_item($format, $section, $cm); } /** * Return structure for edit_module() * * @deprecated since 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return \core_external\external_description */ public static function edit_module_returns() { return new external_value(PARAM_RAW, 'html to replace the current module with'); } /** * Mark the get_module as deprecated. * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return bool */ public static function get_module_is_deprecated() { return true; } /** * Parameters for function get_module() * * @deprecated since 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return external_function_parameters */ public static function get_module_parameters() { return new external_function_parameters( array( 'id' => new external_value(PARAM_INT, 'course module id', VALUE_REQUIRED), 'sectionreturn' => new external_value(PARAM_INT, 'section to return to', VALUE_DEFAULT, null), )); } /** * Returns html for displaying one activity module on course page * * @deprecated since 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @param int $id * @param null|int $sectionreturn * @return string */ public static function get_module($id, $sectionreturn = null) { global $PAGE; // Validate and normalize parameters. $params = self::validate_parameters(self::get_module_parameters(), array('id' => $id, 'sectionreturn' => $sectionreturn)); $id = $params['id']; $sectionreturn = $params['sectionreturn']; // Set of permissions an editing user may have. $contextarray = [ 'moodle/course:update', 'moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/course:sectionvisibility', 'moodle/course:movesections', 'moodle/course:setcurrentsection', ]; $PAGE->set_other_editing_capability($contextarray); // Validate access to the course (note, this is html for the course view page, we don't validate access to the module). list($course, $cm) = get_course_and_cm_from_cmid($id); self::validate_context(context_course::instance($course->id)); $format = course_get_format($course); if (!is_null($sectionreturn)) { $format->set_sectionnum($sectionreturn); } $renderer = $format->get_renderer($PAGE); $modinfo = $format->get_modinfo(); $section = $modinfo->get_section_info($cm->sectionnum); return $renderer->course_section_updated_cm_item($format, $section, $cm); } /** * Return structure for get_module() * * @deprecated since 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return \core_external\external_description */ public static function get_module_returns() { return new external_value(PARAM_RAW, 'html to replace the current module with'); } /** * Mark the edit_section as deprecated. * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return bool */ public static function edit_section_is_deprecated() { return true; } /** * Parameters for function edit_section() * * @deprecated since 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return external_function_parameters */ public static function edit_section_parameters() { return new external_function_parameters( array( 'action' => new external_value(PARAM_ALPHA, 'action: hide, show, stealth, setmarker, removemarker', VALUE_REQUIRED), 'id' => new external_value(PARAM_INT, 'course section id', VALUE_REQUIRED), 'sectionreturn' => new external_value(PARAM_INT, 'section to return to', VALUE_DEFAULT, null), )); } /** * Performs one of the edit section actions * * @deprecated since 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @param string $action * @param int $id section id * @param int $sectionreturn section to return to * @return string */ public static function edit_section($action, $id, $sectionreturn) { global $DB; // Validate and normalize parameters. $params = self::validate_parameters(self::edit_section_parameters(), array('action' => $action, 'id' => $id, 'sectionreturn' => $sectionreturn)); $action = $params['action']; $id = $params['id']; $sr = $params['sectionreturn']; $section = $DB->get_record('course_sections', array('id' => $id), '*', MUST_EXIST); $coursecontext = context_course::instance($section->course); self::validate_context($coursecontext); $rv = course_get_format($section->course)->section_action($section, $action, $sectionreturn); if ($rv) { return json_encode($rv); } else { return null; } } /** * Return structure for edit_section() * * @deprecated since 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return \core_external\external_description */ public static function edit_section_returns() { return new external_value(PARAM_RAW, 'Additional data for javascript (JSON-encoded string)'); } /** * Returns description of method parameters * * @return external_function_parameters */ public static function get_enrolled_courses_by_timeline_classification_parameters() { return new external_function_parameters( array( 'classification' => new external_value(PARAM_ALPHA, 'future, inprogress, or past'), 'limit' => new external_value(PARAM_INT, 'Result set limit', VALUE_DEFAULT, 0), 'offset' => new external_value(PARAM_INT, 'Result set offset', VALUE_DEFAULT, 0), 'sort' => new external_value(PARAM_TEXT, 'Sort string', VALUE_DEFAULT, null), 'customfieldname' => new external_value(PARAM_ALPHANUMEXT, 'Used when classification = customfield', VALUE_DEFAULT, null), 'customfieldvalue' => new external_value(PARAM_RAW, 'Used when classification = customfield', VALUE_DEFAULT, null), 'searchvalue' => new external_value(PARAM_RAW, 'The value a user wishes to search against', VALUE_DEFAULT, null), 'requiredfields' => new core_external\external_multiple_structure( new external_value(PARAM_ALPHANUMEXT, 'Field name to be included from the results', VALUE_DEFAULT), 'Array of the only field names that need to be returned. If empty, all fields will be returned.', VALUE_DEFAULT, [] ), ) ); } /** * Get courses matching the given timeline classification. * * NOTE: The offset applies to the unfiltered full set of courses before the classification * filtering is done. * E.g. * If the user is enrolled in 5 courses: * c1, c2, c3, c4, and c5 * And c4 and c5 are 'future' courses * * If a request comes in for future courses with an offset of 1 it will mean that * c1 is skipped (because the offset applies *before* the classification filtering) * and c4 and c5 will be return. * * @param string $classification past, inprogress, or future * @param int $limit Result set limit * @param int $offset Offset the full course set before timeline classification is applied * @param string|null $sort SQL sort string for results * @param string|null $customfieldname * @param string|null $customfieldvalue * @param string|null $searchvalue * @param array $requiredfields Array of the only field names that need to be returned. If empty, all fields will be returned. * @return array list of courses and warnings */ public static function get_enrolled_courses_by_timeline_classification( string $classification, int $limit = 0, int $offset = 0, ?string $sort = null, ?string $customfieldname = null, ?string $customfieldvalue = null, ?string $searchvalue = null, array $requiredfields = [] ) { global $CFG, $PAGE, $USER; require_once($CFG->dirroot . '/course/lib.php'); $params = self::validate_parameters(self::get_enrolled_courses_by_timeline_classification_parameters(), array( 'classification' => $classification, 'limit' => $limit, 'offset' => $offset, 'sort' => $sort, 'customfieldvalue' => $customfieldvalue, 'searchvalue' => $searchvalue, 'requiredfields' => $requiredfields, ) ); $classification = $params['classification']; $limit = $params['limit']; $offset = $params['offset']; $sort = $params['sort']; $customfieldvalue = $params['customfieldvalue']; $searchvalue = clean_param($params['searchvalue'], PARAM_TEXT); $requiredfields = $params['requiredfields']; switch($classification) { case COURSE_TIMELINE_ALLINCLUDINGHIDDEN: break; case COURSE_TIMELINE_ALL: break; case COURSE_TIMELINE_PAST: break; case COURSE_TIMELINE_INPROGRESS: break; case COURSE_TIMELINE_FUTURE: break; case COURSE_FAVOURITES: break; case COURSE_TIMELINE_HIDDEN: break; case COURSE_TIMELINE_SEARCH: break; case COURSE_CUSTOMFIELD: break; default: throw new invalid_parameter_exception('Invalid classification'); } self::validate_context(context_user::instance($USER->id)); $exporterfields = array_keys(course_summary_exporter::define_properties()); // Get the required properties from the exporter fields based on the required fields. $requiredproperties = array_intersect($exporterfields, $requiredfields); // If the resulting required properties is empty, fall back to the exporter fields. if (empty($requiredproperties)) { $requiredproperties = $exporterfields; } $fields = join(',', $requiredproperties); $hiddencourses = get_hidden_courses_on_timeline(); // If the timeline requires really all courses, get really all courses. if ($classification == COURSE_TIMELINE_ALLINCLUDINGHIDDEN) { $courses = course_get_enrolled_courses_for_logged_in_user(0, $offset, $sort, $fields, COURSE_DB_QUERY_LIMIT); // Otherwise if the timeline requires the hidden courses then restrict the result to only $hiddencourses. } else if ($classification == COURSE_TIMELINE_HIDDEN) { $courses = course_get_enrolled_courses_for_logged_in_user(0, $offset, $sort, $fields, COURSE_DB_QUERY_LIMIT, $hiddencourses); // Otherwise get the requested courses and exclude the hidden courses. } else if ($classification == COURSE_TIMELINE_SEARCH) { // Prepare the search API options. $searchcriteria['search'] = $searchvalue; $options = ['idonly' => true]; $courses = course_get_enrolled_courses_for_logged_in_user_from_search( 0, $offset, $sort, $fields, COURSE_DB_QUERY_LIMIT, $searchcriteria, $options ); } else { $courses = course_get_enrolled_courses_for_logged_in_user(0, $offset, $sort, $fields, COURSE_DB_QUERY_LIMIT, [], $hiddencourses); } $favouritecourseids = []; $ufservice = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($USER->id)); $favourites = $ufservice->find_favourites_by_type('core_course', 'courses'); if ($favourites) { $favouritecourseids = array_map( function($favourite) { return $favourite->itemid; }, $favourites); } if ($classification == COURSE_FAVOURITES) { list($filteredcourses, $processedcount) = course_filter_courses_by_favourites( $courses, $favouritecourseids, $limit ); } else if ($classification == COURSE_CUSTOMFIELD) { list($filteredcourses, $processedcount) = course_filter_courses_by_customfield( $courses, $customfieldname, $customfieldvalue, $limit ); } else { list($filteredcourses, $processedcount) = course_filter_courses_by_timeline_classification( $courses, $classification, $limit ); } $renderer = $PAGE->get_renderer('core'); $formattedcourses = array_map(function($course) use ($renderer, $favouritecourseids) { if ($course == null) { return; } context_helper::preload_from_record($course); $context = context_course::instance($course->id); $isfavourite = false; if (in_array($course->id, $favouritecourseids)) { $isfavourite = true; } $exporter = new course_summary_exporter($course, ['context' => $context, 'isfavourite' => $isfavourite]); return $exporter->export($renderer); }, $filteredcourses); $formattedcourses = array_filter($formattedcourses, function($course) { if ($course != null) { return $course; } }); return [ 'courses' => $formattedcourses, 'nextoffset' => $offset + $processedcount ]; } /** * Returns description of method result value * * @return \core_external\external_description */ public static function get_enrolled_courses_by_timeline_classification_returns() { return new external_single_structure( array( 'courses' => new external_multiple_structure(course_summary_exporter::get_read_structure(), 'Course'), 'nextoffset' => new external_value(PARAM_INT, 'Offset for the next request') ) ); } /** * Returns description of method parameters * * @return external_function_parameters */ public static function set_favourite_courses_parameters() { return new external_function_parameters( array( 'courses' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'course ID'), 'favourite' => new external_value(PARAM_BOOL, 'favourite status') ) ) ) ) ); } /** * Set the course favourite status for an array of courses. * * @param array $courses List with course id's and favourite status. * @return array Array with an array of favourite courses. */ public static function set_favourite_courses( array $courses ) { global $USER; $params = self::validate_parameters(self::set_favourite_courses_parameters(), array( 'courses' => $courses ) ); $warnings = []; $ufservice = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($USER->id)); foreach ($params['courses'] as $course) { $warning = []; $favouriteexists = $ufservice->favourite_exists('core_course', 'courses', $course['id'], \context_course::instance($course['id'])); if ($course['favourite']) { if (!$favouriteexists) { try { $ufservice->create_favourite('core_course', 'courses', $course['id'], \context_course::instance($course['id'])); } catch (Exception $e) { $warning['courseid'] = $course['id']; if ($e instanceof moodle_exception) { $warning['warningcode'] = $e->errorcode; } else { $warning['warningcode'] = $e->getCode(); } $warning['message'] = $e->getMessage(); $warnings[] = $warning; $warnings[] = $warning; } } else { $warning['courseid'] = $course['id']; $warning['warningcode'] = 'coursealreadyfavourited'; $warning['message'] = 'Course already favourited'; $warnings[] = $warning; } } else { if ($favouriteexists) { try { $ufservice->delete_favourite('core_course', 'courses', $course['id'], \context_course::instance($course['id'])); } catch (Exception $e) { $warning['courseid'] = $course['id']; if ($e instanceof moodle_exception) { $warning['warningcode'] = $e->errorcode; } else { $warning['warningcode'] = $e->getCode(); } $warning['message'] = $e->getMessage(); $warnings[] = $warning; $warnings[] = $warning; } } else { $warning['courseid'] = $course['id']; $warning['warningcode'] = 'cannotdeletefavourite'; $warning['message'] = 'Could not delete favourite status for course'; $warnings[] = $warning; } } } return [ 'warnings' => $warnings ]; } /** * Returns description of method result value * * @return \core_external\external_description */ public static function set_favourite_courses_returns() { return new external_single_structure( array( 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.6 */ public static function get_recent_courses_parameters() { return new external_function_parameters( array( 'userid' => new external_value(PARAM_INT, 'id of the user, default to current user', VALUE_DEFAULT, 0), 'limit' => new external_value(PARAM_INT, 'result set limit', VALUE_DEFAULT, 0), 'offset' => new external_value(PARAM_INT, 'Result set offset', VALUE_DEFAULT, 0), 'sort' => new external_value(PARAM_TEXT, 'Sort string', VALUE_DEFAULT, null) ) ); } /** * Get last accessed courses adding additional course information like images. * * @param int $userid User id from which the courses will be obtained * @param int $limit Restrict result set to this amount * @param int $offset Skip this number of records from the start of the result set * @param string|null $sort SQL string for sorting * @return array List of courses * @throws invalid_parameter_exception */ public static function get_recent_courses(int $userid = 0, int $limit = 0, int $offset = 0, ?string $sort = null) { global $USER, $PAGE; if (empty($userid)) { $userid = $USER->id; } $params = self::validate_parameters(self::get_recent_courses_parameters(), array( 'userid' => $userid, 'limit' => $limit, 'offset' => $offset, 'sort' => $sort ) ); $userid = $params['userid']; $limit = $params['limit']; $offset = $params['offset']; $sort = $params['sort']; $usercontext = context_user::instance($userid); self::validate_context($usercontext); if ($userid != $USER->id && !has_capability('moodle/user:viewalldetails', $usercontext)) { return array(); } $courses = course_get_recent_courses($userid, $limit, $offset, $sort); $renderer = $PAGE->get_renderer('core'); $recentcourses = array_map(function($course) use ($renderer) { context_helper::preload_from_record($course); $context = context_course::instance($course->id); $isfavourite = !empty($course->component); $exporter = new course_summary_exporter($course, ['context' => $context, 'isfavourite' => $isfavourite]); return $exporter->export($renderer); }, $courses); return $recentcourses; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.6 */ public static function get_recent_courses_returns() { return new external_multiple_structure(course_summary_exporter::get_read_structure(), 'Courses'); } /** * Returns description of method parameters * * @return external_function_parameters */ public static function get_enrolled_users_by_cmid_parameters() { return new external_function_parameters([ 'cmid' => new external_value(PARAM_INT, 'id of the course module', VALUE_REQUIRED), 'groupid' => new external_value(PARAM_INT, 'id of the group', VALUE_DEFAULT, 0), 'onlyactive' => new external_value(PARAM_BOOL, 'whether to return only active users or all.', VALUE_DEFAULT, false), ]); } /** * Get all users in a course for a given cmid. * * @param int $cmid Course Module id from which the users will be obtained * @param int $groupid Group id from which the users will be obtained * @param bool $onlyactive Whether to return only the active enrolled users or all enrolled users in the course. * @return array List of users */ public static function get_enrolled_users_by_cmid(int $cmid, int $groupid = 0, bool $onlyactive = false) { global $PAGE; $warnings = []; [ 'cmid' => $cmid, 'groupid' => $groupid, 'onlyactive' => $onlyactive, ] = self::validate_parameters(self::get_enrolled_users_by_cmid_parameters(), [ 'cmid' => $cmid, 'groupid' => $groupid, 'onlyactive' => $onlyactive, ]); list($course, $cm) = get_course_and_cm_from_cmid($cmid); $coursecontext = context_course::instance($course->id); self::validate_context($coursecontext); course_require_view_participants($coursecontext); $enrolledusers = get_enrolled_users($coursecontext, '', $groupid, 'u.*', null, 0, 0, $onlyactive); $users = array_map(function ($user) use ($PAGE) { $user->fullname = fullname($user); $userpicture = new user_picture($user); $userpicture->size = 1; $user->profileimage = $userpicture->get_url($PAGE)->out(false); return $user; }, $enrolledusers); sort($users); return [ 'users' => $users, 'warnings' => $warnings, ]; } /** * Returns description of method result value * * @return \core_external\external_description */ public static function get_enrolled_users_by_cmid_returns() { return new external_single_structure([ 'users' => new external_multiple_structure(self::user_description()), 'warnings' => new external_warnings(), ]); } /** * Create user return value description. * * @return \core_external\external_description */ public static function user_description() { $userfields = array( 'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'), 'profileimage' => new external_value(PARAM_URL, 'The location of the users larger image', VALUE_OPTIONAL), 'fullname' => new external_value(PARAM_TEXT, 'The full name of the user', VALUE_OPTIONAL), 'firstname' => new external_value( core_user::get_property_type('firstname'), 'The first name(s) of the user', VALUE_OPTIONAL), 'lastname' => new external_value( core_user::get_property_type('lastname'), 'The family name of the user', VALUE_OPTIONAL), ); return new external_single_structure($userfields); } /** * Returns description of method parameters. * * @return external_function_parameters */ public static function add_content_item_to_user_favourites_parameters() { return new external_function_parameters([ 'componentname' => new external_value(PARAM_TEXT, 'frankenstyle name of the component to which the content item belongs', VALUE_REQUIRED), 'contentitemid' => new external_value(PARAM_INT, 'id of the content item', VALUE_REQUIRED, '', NULL_NOT_ALLOWED) ]); } /** * Add a content item to a user's favourites. * * @param string $componentname the name of the component from which this content item originates. * @param int $contentitemid the id of the content item. * @return stdClass the exporter content item. */ public static function add_content_item_to_user_favourites(string $componentname, int $contentitemid) { global $USER; [ 'componentname' => $componentname, 'contentitemid' => $contentitemid, ] = self::validate_parameters(self::add_content_item_to_user_favourites_parameters(), [ 'componentname' => $componentname, 'contentitemid' => $contentitemid, ] ); self::validate_context(context_user::instance($USER->id)); $contentitemservice = \core_course\local\factory\content_item_service_factory::get_content_item_service(); return $contentitemservice->add_to_user_favourites($USER, $componentname, $contentitemid); } /** * Returns description of method result value. * * @return \core_external\external_description */ public static function add_content_item_to_user_favourites_returns() { return \core_course\local\exporters\course_content_item_exporter::get_read_structure(); } /** * Returns description of method parameters. * * @return external_function_parameters */ public static function remove_content_item_from_user_favourites_parameters() { return new external_function_parameters([ 'componentname' => new external_value(PARAM_TEXT, 'frankenstyle name of the component to which the content item belongs', VALUE_REQUIRED), 'contentitemid' => new external_value(PARAM_INT, 'id of the content item', VALUE_REQUIRED, '', NULL_NOT_ALLOWED), ]); } /** * Remove a content item from a user's favourites. * * @param string $componentname the name of the component from which this content item originates. * @param int $contentitemid the id of the content item. * @return stdClass the exported content item. */ public static function remove_content_item_from_user_favourites(string $componentname, int $contentitemid) { global $USER; [ 'componentname' => $componentname, 'contentitemid' => $contentitemid, ] = self::validate_parameters(self::remove_content_item_from_user_favourites_parameters(), [ 'componentname' => $componentname, 'contentitemid' => $contentitemid, ] ); self::validate_context(context_user::instance($USER->id)); $contentitemservice = \core_course\local\factory\content_item_service_factory::get_content_item_service(); return $contentitemservice->remove_from_user_favourites($USER, $componentname, $contentitemid); } /** * Returns description of method result value. * * @return \core_external\external_description */ public static function remove_content_item_from_user_favourites_returns() { return \core_course\local\exporters\course_content_item_exporter::get_read_structure(); } /** * Returns description of method result value * * @return \core_external\external_description */ public static function get_course_content_items_returns() { return new external_single_structure([ 'content_items' => new external_multiple_structure( \core_course\local\exporters\course_content_item_exporter::get_read_structure() ), ]); } /** * Returns description of method parameters * * @return external_function_parameters */ public static function get_course_content_items_parameters() { return new external_function_parameters([ 'courseid' => new external_value(PARAM_INT, 'ID of the course', VALUE_REQUIRED), 'sectionnum' => new external_value(PARAM_INT, 'Number of the section', VALUE_DEFAULT), ]); } /** * Given a course ID fetch all accessible modules for that course * * @param int $courseid The course we want to fetch the modules for * @param int|null $sectionnum The section we want to fetch the modules for * @return array Contains array of modules and their metadata */ public static function get_course_content_items(int $courseid, ?int $sectionnum = null) { global $USER; [ 'courseid' => $courseid, 'sectionnum' => $sectionnum, ] = self::validate_parameters(self::get_course_content_items_parameters(), [ 'courseid' => $courseid, 'sectionnum' => $sectionnum, ]); $coursecontext = context_course::instance($courseid); self::validate_context($coursecontext); $course = get_course($courseid); // Get section_info object with all delegation information. $sectioninfo = get_fast_modinfo($course)->get_section_info($sectionnum); $contentitemservice = \core_course\local\factory\content_item_service_factory::get_content_item_service(); $contentitems = $contentitemservice->get_content_items_for_user_in_course($USER, $course, [], $sectioninfo); return ['content_items' => $contentitems]; } /** * Returns description of method parameters. * * @return external_function_parameters */ public static function toggle_activity_recommendation_parameters() { return new external_function_parameters([ 'area' => new external_value(PARAM_TEXT, 'The favourite area (itemtype)', VALUE_REQUIRED), 'id' => new external_value(PARAM_INT, 'id of the activity or whatever', VALUE_REQUIRED), ]); } /** * Update the recommendation for an activity item. * * @param string $area identifier for this activity. * @param int $id Associated id. This is needed in conjunction with the area to find the recommendation. * @return array some warnings or something. */ public static function toggle_activity_recommendation(string $area, int $id): array { ['area' => $area, 'id' => $id] = self::validate_parameters(self::toggle_activity_recommendation_parameters(), ['area' => $area, 'id' => $id]); $context = context_system::instance(); self::validate_context($context); require_capability('moodle/course:recommendactivity', $context); $manager = \core_course\local\factory\content_item_service_factory::get_content_item_service(); $status = $manager->toggle_recommendation($area, $id); return ['id' => $id, 'area' => $area, 'status' => $status]; } /** * Returns warnings. * * @return \core_external\external_description */ public static function toggle_activity_recommendation_returns() { return new external_single_structure( [ 'id' => new external_value(PARAM_INT, 'id of the activity or whatever'), 'area' => new external_value(PARAM_TEXT, 'The favourite area (itemtype)'), 'status' => new external_value(PARAM_BOOL, 'If created or deleted'), ] ); } /** * Returns description of method parameters * * @return external_function_parameters */ public static function get_activity_chooser_footer_parameters() { return new external_function_parameters([ 'courseid' => new external_value(PARAM_INT, 'ID of the course', VALUE_REQUIRED), 'sectionid' => new external_value(PARAM_INT, 'ID of the section', VALUE_REQUIRED), ]); } /** * Given a course ID we need to build up a footre for the chooser. * * @param int $courseid The course we want to fetch the modules for * @param int $sectionid The section we want to fetch the modules for * @return array */ public static function get_activity_chooser_footer(int $courseid, int $sectionid) { [ 'courseid' => $courseid, 'sectionid' => $sectionid, ] = self::validate_parameters(self::get_activity_chooser_footer_parameters(), [ 'courseid' => $courseid, 'sectionid' => $sectionid, ]); $coursecontext = context_course::instance($courseid); self::validate_context($coursecontext); // The active plugin must be set, and be present on the site. $activeplugin = clean_param( get_config('core', 'activitychooseractivefooter'), PARAM_COMPONENT, ); if ( $activeplugin !== COURSE_CHOOSER_FOOTER_NONE && !empty($activeplugin) && core_component::get_component_directory($activeplugin) !== null ) { $footerdata = component_callback($activeplugin, 'custom_chooser_footer', [$courseid, $sectionid]); return [ 'footer' => true, 'customfooterjs' => $footerdata->get_footer_js_file(), 'customfootertemplate' => $footerdata->get_footer_template(), 'customcarouseltemplate' => $footerdata->get_carousel_template(), ]; } else { return [ 'footer' => false, ]; } } /** * Returns description of method result value * * @return \core_external\external_description */ public static function get_activity_chooser_footer_returns() { return new external_single_structure( [ 'footer' => new external_value(PARAM_BOOL, 'Is a footer being return by this request?', VALUE_REQUIRED), 'customfooterjs' => new external_value(PARAM_RAW, 'The path to the plugin JS file', VALUE_OPTIONAL), 'customfootertemplate' => new external_value(PARAM_RAW, 'The prerendered footer', VALUE_OPTIONAL), 'customcarouseltemplate' => new external_value(PARAM_RAW, 'Either "" or the prerendered carousel page', VALUE_OPTIONAL), ] ); } } PK$9\3Q:JJ renderer.phpnu[. /** * Renderer for use with the course section and all the goodness that falls * within it. * * This renderer should contain methods useful to courses, and categories. * * @package moodlecore * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * The core course renderer * * Can be retrieved with the following: * $renderer = $PAGE->get_renderer('core','course'); */ class core_course_renderer extends plugin_renderer_base { const COURSECAT_SHOW_COURSES_NONE = 0; /* do not show courses at all */ const COURSECAT_SHOW_COURSES_COUNT = 5; /* do not show courses but show number of courses next to category name */ const COURSECAT_SHOW_COURSES_COLLAPSED = 10; const COURSECAT_SHOW_COURSES_AUTO = 15; /* will choose between collapsed and expanded automatically */ const COURSECAT_SHOW_COURSES_EXPANDED = 20; const COURSECAT_SHOW_COURSES_EXPANDED_WITH_CAT = 30; const COURSECAT_TYPE_CATEGORY = 0; const COURSECAT_TYPE_COURSE = 1; /** * A cache of strings * @var stdClass */ protected $strings; /** * Whether a category content is being initially rendered with children. This is mainly used by the * core_course_renderer::corsecat_tree() to render the appropriate action for the Expand/Collapse all link on * page load. * @var bool */ protected $categoryexpandedonload = false; /** * Override the constructor so that we can initialise the string cache * * @param moodle_page $page * @param string $target */ public function __construct(moodle_page $page, $target) { $this->strings = new stdClass; $courseid = $page->course->id; parent::__construct($page, $target); } /** * Renders course info box. * * @param stdClass $course * @return string */ public function course_info_box(stdClass $course) { $content = ''; $content .= $this->output->box_start('generalbox info'); $chelper = new coursecat_helper(); $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_EXPANDED); $content .= $this->coursecat_coursebox($chelper, $course); $content .= $this->output->box_end(); return $content; } /** * Renders course info box. * * @param stdClass $course course object * @param string[] $widgets array of enrolment widgets * @param \core\url|null $returnurl return url * @return string */ public function enrolment_options(stdClass $course, array $widgets, ?\core\url $returnurl = null): string { if (!$widgets) { if (isguestuser()) { $message = get_string('noguestaccess', 'enrol'); $continuebutton = $this->output->continue_button(get_login_url()); } else if ($returnurl) { $message = get_string('notenrollable', 'enrol'); $continuebutton = $this->output->continue_button($returnurl); } else { $url = get_local_referer(false); if (empty($url)) { $url = new moodle_url('/index.php'); } $message = get_string('notenrollable', 'enrol'); $continuebutton = $this->output->continue_button($url); } } $courseinfobox = $this->course_info_box($course); $templatecontext = [ 'heading' => get_string('enrolmentoptions', 'enrol'), 'courseinfobox' => $courseinfobox, 'widgets' => array_values($widgets), 'message' => $message ?? '', 'continuebutton' => $continuebutton ?? '', ]; return $this->render_from_template('core_enrol/enrolment_options', $templatecontext); } /** * Renderers a structured array of courses and categories into a nice XHTML tree structure. * * @deprecated since 2.5 * * @param array $ignored argument ignored * @return string */ final public function course_category_tree(array $ignored) { debugging('Function core_course_renderer::course_category_tree() is deprecated, please use frontpage_combo_list()', DEBUG_DEVELOPER); return $this->frontpage_combo_list(); } /** * Renderers a category for use with course_category_tree * * @deprecated since 2.5 * * @param array $category * @param int $depth * @return string */ final protected function course_category_tree_category(stdClass $category, $depth=1) { debugging('Function core_course_renderer::course_category_tree_category() is deprecated', DEBUG_DEVELOPER); return ''; } /** * Render a modchooser. * * @param renderable $modchooser The chooser. * @return string */ public function render_modchooser(renderable $modchooser) { return $this->render_from_template('core_course/modchooser', $modchooser->export_for_template($this)); } /** * Build the HTML for the module chooser javascript popup. * * @param int $courseid The course id to fetch modules for. * @return string */ public function course_activitychooser($courseid) { if (!$this->page->requires->should_create_one_time_item_now('core_course_modchooser')) { return ''; } // Build an object of config settings that we can then hook into in the Activity Chooser. $chooserconfig = (object) [ 'tabmode' => get_config('core', 'activitychoosertabmode'), ]; $this->page->requires->js_call_amd('core_course/activitychooser', 'init', [$courseid, $chooserconfig]); return ''; } /** * Build the HTML for a specified set of modules * * @param array $modules A set of modules as used by the * course_modchooser_module function * @return string The composed HTML for the module */ protected function course_modchooser_module_types($modules) { debugging('Method core_course_renderer::course_modchooser_module_types() is deprecated, ' . 'see core_course_renderer::render_modchooser().', DEBUG_DEVELOPER); return ''; } /** * Return the HTML for the specified module adding any required classes * * @param object $module An object containing the title, and link. An * icon, and help text may optionally be specified. If the module * contains subtypes in the types option, then these will also be * displayed. * @param array $classes Additional classes to add to the encompassing * div element * @return string The composed HTML for the module */ protected function course_modchooser_module($module, $classes = array('option')) { debugging('Method core_course_renderer::course_modchooser_module() is deprecated, ' . 'see core_course_renderer::render_modchooser().', DEBUG_DEVELOPER); return ''; } protected function course_modchooser_title($title, $identifier = null) { debugging('Method core_course_renderer::course_modchooser_title() is deprecated, ' . 'see core_course_renderer::render_modchooser().', DEBUG_DEVELOPER); return ''; } /** * Renders HTML for the menus to add activities and resources to the current course * * Renders the ajax control (the link which when clicked produces the activity chooser modal). No noscript fallback. * * @param stdClass $course * @param int $section relative section number (field course_sections.section) * @param int $sectionreturn The section to link back to * @param array $displayoptions additional display options, for example blocks add * option 'inblock' => true, suggesting to display controls vertically * @return string */ function course_section_add_cm_control($course, $section, $sectionreturn = null, $displayoptions = array()) { // Check to see if user can add menus. if (!has_capability('moodle/course:manageactivities', context_course::instance($course->id)) || !$this->page->user_is_editing()) { return ''; } $sectioninfo = get_fast_modinfo($course)->get_section_info($section); $activitychooserbutton = new \core_course\output\activitychooserbutton($sectioninfo, null, $sectionreturn); // Load the JS for the modal. $this->course_activitychooser($course->id); return $this->render_from_template( 'core_courseformat/local/content/divider', [ 'content' => $this->render($activitychooserbutton), 'extraclasses' => 'always-visible my-3', ] ); } /** * Renders html to display a course search form * * @param string $value default value to populate the search field * @return string */ public function course_search_form($value = '') { $data = [ 'action' => \core_search\manager::get_course_search_url(), 'btnclass' => 'btn-primary', 'inputname' => 'q', 'searchstring' => get_string('searchcourses'), 'hiddenfields' => (object) ['name' => 'areaids', 'value' => 'core_course-course'], 'query' => $value ]; return $this->render_from_template('core/search_input', $data); } /** * Message displayed to the user when they try to access unavailable activity following URL * * This method is a very simplified version of {@link course_section_cm()} to be part of the error * notification only. It also does not check if module is visible on course page or not. * * The message will be displayed inside notification! * * @param cm_info $cm * @return string */ public function course_section_cm_unavailable_error_message(cm_info $cm) { if ($cm->uservisible) { return null; } if (!$cm->availableinfo) { return get_string('activityiscurrentlyhidden'); } $altname = get_accesshide(' ' . $cm->modfullname); $name = html_writer::empty_tag('img', ['src' => $cm->get_icon_url(), 'class' => 'activityicon', 'alt' => '']) . html_writer::tag('span', ' '.$cm->get_formatted_name() . $altname, array('class' => 'instancename')); $formattedinfo = \core_availability\info::format_info($cm->availableinfo, $cm->get_course()); return html_writer::div($name, 'activityinstance-error') . html_writer::div($formattedinfo, 'availabilityinfo-error'); } /** * Displays a custom list of courses with paging bar if necessary * * If $paginationurl is specified but $totalcount is not, the link 'View more' * appears under the list. * * If both $paginationurl and $totalcount are specified, and $totalcount is * bigger than count($courses), a paging bar is displayed above and under the * courses list. * * @param array $courses array of course records (or instances of core_course_list_element) to show on this page * @param bool $showcategoryname whether to add category name to the course description * @param string $additionalclasses additional CSS classes to add to the div.courses * @param moodle_url $paginationurl url to view more or url to form links to the other pages in paging bar * @param int $totalcount total number of courses on all pages, if omitted $paginationurl will be displayed as 'View more' link * @param int $page current page number (defaults to 0 referring to the first page) * @param int $perpage number of records per page (defaults to $CFG->coursesperpage) * @return string */ public function courses_list($courses, $showcategoryname = false, $additionalclasses = null, $paginationurl = null, $totalcount = null, $page = 0, $perpage = null) { global $CFG; // create instance of coursecat_helper to pass display options to function rendering courses list $chelper = new coursecat_helper(); if ($showcategoryname) { $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_EXPANDED_WITH_CAT); } else { $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_EXPANDED); } if ($totalcount !== null && $paginationurl !== null) { // add options to display pagination if ($perpage === null) { $perpage = $CFG->coursesperpage; } $chelper->set_courses_display_options(array( 'limit' => $perpage, 'offset' => ((int)$page) * $perpage, 'paginationurl' => $paginationurl, )); } else if ($paginationurl !== null) { // add options to display 'View more' link $chelper->set_courses_display_options(array('viewmoreurl' => $paginationurl)); $totalcount = count($courses) + 1; // has to be bigger than count($courses) otherwise link will not be displayed } $chelper->set_attributes(array('class' => $additionalclasses)); $content = $this->coursecat_courses($chelper, $courses, $totalcount); return $content; } /** * Returns HTML to display course name. * * @param coursecat_helper $chelper * @param core_course_list_element $course * @return string */ protected function course_name(coursecat_helper $chelper, core_course_list_element $course): string { $content = ''; if ($chelper->get_show_courses() >= self::COURSECAT_SHOW_COURSES_EXPANDED) { $nametag = 'h3'; } else { $nametag = 'div'; } $coursename = $chelper->get_course_formatted_name($course); $coursenamelink = html_writer::link(new moodle_url('/course/view.php', ['id' => $course->id]), $coursename, ['class' => $course->visible ? 'aalink' : 'aalink dimmed']); $content .= html_writer::tag($nametag, $coursenamelink, ['class' => 'coursename']); // If we display course in collapsed form but the course has summary or course contacts, display the link to the info page. $content .= html_writer::start_tag('div', ['class' => 'moreinfo']); if ($chelper->get_show_courses() < self::COURSECAT_SHOW_COURSES_EXPANDED) { if ($course->has_summary() || $course->has_course_contacts() || $course->has_course_overviewfiles() || $course->has_custom_fields()) { $url = new moodle_url('/course/info.php', ['id' => $course->id]); $image = $this->output->pix_icon('i/info', $this->strings->summary); $content .= html_writer::link($url, $image, ['title' => $this->strings->summary]); // Make sure JS file to expand course content is included. $this->coursecat_include_js(); } } $content .= html_writer::end_tag('div'); return $content; } /** * Returns HTML to display course enrolment icons. * * @param core_course_list_element $course * @return string */ protected function course_enrolment_icons(core_course_list_element $course): string { $content = ''; if ($icons = enrol_get_course_info_icons($course)) { $content .= html_writer::start_tag('div', ['class' => 'enrolmenticons']); foreach ($icons as $icon) { $content .= $this->render($icon); } $content .= html_writer::end_tag('div'); } return $content; } /** * Displays one course in the list of courses. * * This is an internal function, to display an information about just one course * please use {@link core_course_renderer::course_info_box()} * * @param coursecat_helper $chelper various display options * @param core_course_list_element|stdClass $course * @param string $additionalclasses additional classes to add to the main
tag (usually * depend on the course position in list - first/last/even/odd) * @return string */ protected function coursecat_coursebox(coursecat_helper $chelper, $course, $additionalclasses = '') { if (!isset($this->strings->summary)) { $this->strings->summary = get_string('summary'); } if ($chelper->get_show_courses() <= self::COURSECAT_SHOW_COURSES_COUNT) { return ''; } if ($course instanceof stdClass) { $course = new core_course_list_element($course); } $content = ''; $classes = trim('coursebox clearfix '. $additionalclasses); if ($chelper->get_show_courses() < self::COURSECAT_SHOW_COURSES_EXPANDED) { $classes .= ' collapsed'; } // .coursebox $content .= html_writer::start_tag('div', array( 'class' => $classes, 'data-courseid' => $course->id, 'data-type' => self::COURSECAT_TYPE_COURSE, )); $content .= html_writer::start_tag('div', array('class' => 'info')); $content .= $this->course_name($chelper, $course); $content .= $this->course_enrolment_icons($course); $content .= html_writer::end_tag('div'); $content .= html_writer::start_tag('div', array('class' => 'content')); $content .= $this->coursecat_coursebox_content($chelper, $course); $content .= html_writer::end_tag('div'); $content .= html_writer::end_tag('div'); // .coursebox return $content; } /** * Returns HTML to display course summary. * * @param coursecat_helper $chelper * @param core_course_list_element $course * @return string */ protected function course_summary(coursecat_helper $chelper, core_course_list_element $course): string { $content = ''; if ($course->has_summary()) { $content .= html_writer::start_tag('div', ['class' => 'summary']); $content .= $chelper->get_course_formatted_summary($course, array('overflowdiv' => true, 'noclean' => true, 'para' => false)); $content .= html_writer::end_tag('div'); } return $content; } /** * Returns HTML to display course contacts. * * @param core_course_list_element $course * @return string */ protected function course_contacts(core_course_list_element $course) { $content = ''; if ($course->has_course_contacts()) { $content .= html_writer::start_tag('ul', ['class' => 'teachers']); foreach ($course->get_course_contacts() as $coursecontact) { $rolenames = array_map(function ($role) { return $role->displayname; }, $coursecontact['roles']); $name = html_writer::tag('span', implode(", ", $rolenames).': ', ['class' => 'fw-bold']); $name .= html_writer::link( \core_user::get_profile_url($coursecontact['user'], context_system::instance()), $coursecontact['username'] ); $content .= html_writer::tag('li', $name); } $content .= html_writer::end_tag('ul'); } return $content; } /** * Returns HTML to display course overview files. * * @param core_course_list_element $course * @return string */ protected function course_overview_files(core_course_list_element $course): string { global $CFG; $contentimages = $contentfiles = ''; foreach ($course->get_course_overviewfiles() as $file) { $isimage = $file->is_valid_image(); $url = moodle_url::make_file_url("$CFG->wwwroot/pluginfile.php", '/' . $file->get_contextid() . '/' . $file->get_component() . '/' . $file->get_filearea() . $file->get_filepath() . $file->get_filename(), !$isimage); if ($isimage) { $contentimages .= html_writer::tag('div', html_writer::empty_tag('img', ['src' => $url, 'alt' => '']), ['class' => 'courseimage']); } else { $image = $this->output->pix_icon(file_file_icon($file), $file->get_filename(), 'moodle'); $filename = html_writer::tag('span', $image, ['class' => 'fp-icon']). html_writer::tag('span', $file->get_filename(), ['class' => 'fp-filename']); $contentfiles .= html_writer::tag('span', html_writer::link($url, $filename), ['class' => 'coursefile fp-filename-icon text-break']); } } return $contentimages . $contentfiles; } /** * Returns HTML to display course category name. * * @param coursecat_helper $chelper * @param core_course_list_element $course * @return string */ protected function course_category_name(coursecat_helper $chelper, core_course_list_element $course): string { $content = ''; // Display course category if necessary (for example in search results). if ($chelper->get_show_courses() == self::COURSECAT_SHOW_COURSES_EXPANDED_WITH_CAT) { if ($cat = core_course_category::get($course->category, IGNORE_MISSING)) { $content .= html_writer::start_tag('div', ['class' => 'coursecat']); $content .= html_writer::tag('span', get_string('category').': ', ['class' => 'fw-bold']); $content .= html_writer::link(new moodle_url('/course/index.php', ['categoryid' => $cat->id]), $cat->get_formatted_name(), ['class' => $cat->visible ? '' : 'dimmed']); $content .= html_writer::end_tag('div'); } } return $content; } /** * Returns HTML to display course custom fields. * * @param core_course_list_element $course * @return string */ protected function course_custom_fields(core_course_list_element $course): string { $content = ''; if ($course->has_custom_fields()) { $handler = core_course\customfield\course_handler::create(); $customfields = $handler->display_custom_fields_data($course->get_custom_fields()); $content .= \html_writer::tag('div', $customfields, ['class' => 'customfields-container']); } return $content; } /** * Returns HTML to display course content (summary, course contacts and optionally category name) * * This method is called from coursecat_coursebox() and may be re-used in AJAX * * @param coursecat_helper $chelper various display options * @param stdClass|core_course_list_element $course * @return string */ protected function coursecat_coursebox_content(coursecat_helper $chelper, $course) { if ($chelper->get_show_courses() < self::COURSECAT_SHOW_COURSES_EXPANDED) { return ''; } if ($course instanceof stdClass) { $course = new core_course_list_element($course); } $content = \html_writer::start_tag('div', ['class' => 'd-flex']); $content .= $this->course_overview_files($course); $content .= \html_writer::start_tag('div', ['class' => 'flex-grow-1']); $content .= $this->course_summary($chelper, $course); $content .= $this->course_contacts($course); $content .= $this->course_category_name($chelper, $course); $content .= $this->course_custom_fields($course); $content .= \html_writer::end_tag('div'); $content .= \html_writer::end_tag('div'); return $content; } /** * Renders the list of courses * * This is internal function, please use {@link core_course_renderer::courses_list()} or another public * method from outside of the class * * If list of courses is specified in $courses; the argument $chelper is only used * to retrieve display options and attributes, only methods get_show_courses(), * get_courses_display_option() and get_and_erase_attributes() are called. * * @param coursecat_helper $chelper various display options * @param array $courses the list of courses to display * @param int|null $totalcount total number of courses (affects display mode if it is AUTO or pagination if applicable), * defaulted to count($courses) * @return string */ protected function coursecat_courses(coursecat_helper $chelper, $courses, $totalcount = null) { global $CFG; if ($totalcount === null) { $totalcount = count($courses); } if (!$totalcount) { // Courses count is cached during courses retrieval. return ''; } if ($chelper->get_show_courses() == self::COURSECAT_SHOW_COURSES_AUTO) { // In 'auto' course display mode we analyse if number of courses is more or less than $CFG->courseswithsummarieslimit if ($totalcount <= $CFG->courseswithsummarieslimit) { $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_EXPANDED); } else { $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_COLLAPSED); } } // prepare content of paging bar if it is needed $paginationurl = $chelper->get_courses_display_option('paginationurl'); $paginationallowall = $chelper->get_courses_display_option('paginationallowall'); if ($totalcount > count($courses)) { // there are more results that can fit on one page if ($paginationurl) { // the option paginationurl was specified, display pagingbar $perpage = $chelper->get_courses_display_option('limit', $CFG->coursesperpage); $page = $chelper->get_courses_display_option('offset') / $perpage; $pagingbar = $this->paging_bar($totalcount, $page, $perpage, $paginationurl->out(false, array('perpage' => $perpage))); if ($paginationallowall) { $pagingbar .= html_writer::tag('div', html_writer::link($paginationurl->out(false, array('perpage' => 'all')), get_string('showall', '', $totalcount)), array('class' => 'paging paging-showall')); } } else if ($viewmoreurl = $chelper->get_courses_display_option('viewmoreurl')) { // the option for 'View more' link was specified, display more link $viewmoretext = $chelper->get_courses_display_option('viewmoretext', new lang_string('viewmore')); $morelink = html_writer::tag( 'div', html_writer::link($viewmoreurl, $viewmoretext, ['class' => 'btn btn-secondary']), ['class' => 'paging paging-morelink'] ); } } else if (($totalcount > $CFG->coursesperpage) && $paginationurl && $paginationallowall) { // there are more than one page of results and we are in 'view all' mode, suggest to go back to paginated view mode $pagingbar = html_writer::tag('div', html_writer::link($paginationurl->out(false, array('perpage' => $CFG->coursesperpage)), get_string('showperpage', '', $CFG->coursesperpage)), array('class' => 'paging paging-showperpage')); } // display list of courses $attributes = $chelper->get_and_erase_attributes('courses'); $content = html_writer::start_tag('div', $attributes); if (!empty($pagingbar)) { $content .= $pagingbar; } $coursecount = 0; foreach ($courses as $course) { $coursecount ++; $classes = ($coursecount%2) ? 'odd' : 'even'; if ($coursecount == 1) { $classes .= ' first'; } if ($coursecount >= count($courses)) { $classes .= ' last'; } $content .= $this->coursecat_coursebox($chelper, $course, $classes); } if (!empty($pagingbar)) { $content .= $pagingbar; } if (!empty($morelink)) { $content .= $morelink; } $content .= html_writer::end_tag('div'); // .courses return $content; } /** * Renders the list of subcategories in a category * * @param coursecat_helper $chelper various display options * @param core_course_category $coursecat * @param int $depth depth of the category in the current tree * @return string */ protected function coursecat_subcategories(coursecat_helper $chelper, $coursecat, $depth) { global $CFG; $subcategories = array(); if (!$chelper->get_categories_display_option('nodisplay')) { $subcategories = $coursecat->get_children($chelper->get_categories_display_options()); } $totalcount = $coursecat->get_children_count(); if (!$totalcount) { // Note that we call core_course_category::get_children_count() AFTER core_course_category::get_children() // to avoid extra DB requests. // Categories count is cached during children categories retrieval. return ''; } // prepare content of paging bar or more link if it is needed $paginationurl = $chelper->get_categories_display_option('paginationurl'); $paginationallowall = $chelper->get_categories_display_option('paginationallowall'); if ($totalcount > count($subcategories)) { if ($paginationurl) { // the option 'paginationurl was specified, display pagingbar $perpage = $chelper->get_categories_display_option('limit', $CFG->coursesperpage); $page = $chelper->get_categories_display_option('offset') / $perpage; $pagingbar = $this->paging_bar($totalcount, $page, $perpage, $paginationurl->out(false, array('perpage' => $perpage))); if ($paginationallowall) { $pagingbar .= html_writer::tag('div', html_writer::link($paginationurl->out(false, array('perpage' => 'all')), get_string('showall', '', $totalcount)), array('class' => 'paging paging-showall')); } } else if ($viewmoreurl = $chelper->get_categories_display_option('viewmoreurl')) { // the option 'viewmoreurl' was specified, display more link (if it is link to category view page, add category id) if ($viewmoreurl->compare(new moodle_url('/course/index.php'), URL_MATCH_BASE)) { $viewmoreurl->param('categoryid', $coursecat->id); } $viewmoretext = $chelper->get_categories_display_option('viewmoretext', new lang_string('viewmore')); $morelink = html_writer::tag('div', html_writer::link($viewmoreurl, $viewmoretext), array('class' => 'paging paging-morelink')); } } else if (($totalcount > $CFG->coursesperpage) && $paginationurl && $paginationallowall) { // there are more than one page of results and we are in 'view all' mode, suggest to go back to paginated view mode $pagingbar = html_writer::tag('div', html_writer::link($paginationurl->out(false, array('perpage' => $CFG->coursesperpage)), get_string('showperpage', '', $CFG->coursesperpage)), array('class' => 'paging paging-showperpage')); } // display list of subcategories $content = html_writer::start_tag('div', array('class' => 'subcategories')); if (!empty($pagingbar)) { $content .= $pagingbar; } foreach ($subcategories as $subcategory) { $content .= $this->coursecat_category($chelper, $subcategory, $depth + 1); } if (!empty($pagingbar)) { $content .= $pagingbar; } if (!empty($morelink)) { $content .= $morelink; } $content .= html_writer::end_tag('div'); return $content; } /** * Make sure that javascript file for AJAX expanding of courses and categories content is included */ protected function coursecat_include_js() { if (!$this->page->requires->should_create_one_time_item_now('core_course_categoryexpanderjsinit')) { return; } // We must only load this module once. $this->page->requires->yui_module('moodle-course-categoryexpander', 'Y.Moodle.course.categoryexpander.init'); } /** * Returns HTML to display the subcategories and courses in the given category * * This method is re-used by AJAX to expand content of not loaded category * * @param coursecat_helper $chelper various display options * @param core_course_category $coursecat * @param int $depth depth of the category in the current tree * @return string */ protected function coursecat_category_content(coursecat_helper $chelper, $coursecat, $depth) { $content = ''; // Subcategories $content .= $this->coursecat_subcategories($chelper, $coursecat, $depth); // AUTO show courses: Courses will be shown expanded if this is not nested category, // and number of courses no bigger than $CFG->courseswithsummarieslimit. $showcoursesauto = $chelper->get_show_courses() == self::COURSECAT_SHOW_COURSES_AUTO; if ($showcoursesauto && $depth) { // this is definitely collapsed mode $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_COLLAPSED); } // Courses if ($chelper->get_show_courses() > core_course_renderer::COURSECAT_SHOW_COURSES_COUNT) { $courses = array(); if (!$chelper->get_courses_display_option('nodisplay')) { $courses = $coursecat->get_courses($chelper->get_courses_display_options()); } if ($viewmoreurl = $chelper->get_courses_display_option('viewmoreurl')) { // the option for 'View more' link was specified, display more link (if it is link to category view page, add category id) if ($viewmoreurl->compare(new moodle_url('/course/index.php'), URL_MATCH_BASE)) { $chelper->set_courses_display_option('viewmoreurl', new moodle_url($viewmoreurl, array('categoryid' => $coursecat->id))); } } $content .= $this->coursecat_courses($chelper, $courses, $coursecat->get_courses_count()); } if ($showcoursesauto) { // restore the show_courses back to AUTO $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_AUTO); } return $content; } /** * Returns HTML to display a course category as a part of a tree * * This is an internal function, to display a particular category and all its contents * use {@link core_course_renderer::course_category()} * * @param coursecat_helper $chelper various display options * @param core_course_category $coursecat * @param int $depth depth of this category in the current tree * @return string */ protected function coursecat_category(coursecat_helper $chelper, $coursecat, $depth) { // open category tag $classes = array('category'); if (empty($coursecat->visible)) { $classes[] = 'dimmed_category'; } if ($chelper->get_subcat_depth() > 0 && $depth >= $chelper->get_subcat_depth()) { // do not load content $categorycontent = ''; $classes[] = 'notloaded'; if ($coursecat->get_children_count() || ($chelper->get_show_courses() >= self::COURSECAT_SHOW_COURSES_COLLAPSED && $coursecat->get_courses_count())) { $classes[] = 'with_children'; $classes[] = 'collapsed'; } } else { // load category content $categorycontent = $this->coursecat_category_content($chelper, $coursecat, $depth); $classes[] = 'loaded'; if (!empty($categorycontent)) { $classes[] = 'with_children'; // Category content loaded with children. $this->categoryexpandedonload = true; } } // Make sure JS file to expand category content is included. $this->coursecat_include_js(); $content = html_writer::start_tag('div', array( 'class' => join(' ', $classes), 'data-categoryid' => $coursecat->id, 'data-depth' => $depth, 'data-showcourses' => $chelper->get_show_courses(), 'data-type' => self::COURSECAT_TYPE_CATEGORY, )); // category name $categoryname = $coursecat->get_formatted_name(); $categoryname = html_writer::link(new moodle_url('/course/index.php', array('categoryid' => $coursecat->id)), $categoryname); if ($chelper->get_show_courses() == self::COURSECAT_SHOW_COURSES_COUNT && ($coursescount = $coursecat->get_courses_count())) { $categoryname .= html_writer::tag('span', ' ('. $coursescount.')', array('title' => get_string('numberofcourses'), 'class' => 'numberofcourse')); } $content .= html_writer::start_tag('div', array('class' => 'info')); $content .= html_writer::tag(($depth > 1) ? 'h4' : 'h3', $categoryname, array('class' => 'categoryname aabtn')); $content .= html_writer::end_tag('div'); // .info // add category content to the output $content .= html_writer::tag('div', $categorycontent, array('class' => 'content')); $content .= html_writer::end_tag('div'); // .category // Return the course category tree HTML return $content; } /** * Returns HTML to display a tree of subcategories and courses in the given category * * @param coursecat_helper $chelper various display options * @param core_course_category $coursecat top category (this category's name and description will NOT be added to the tree) * @return string */ protected function coursecat_tree(coursecat_helper $chelper, $coursecat) { // Reset the category expanded flag for this course category tree first. $this->categoryexpandedonload = false; $categorycontent = $this->coursecat_category_content($chelper, $coursecat, 0); if (empty($categorycontent)) { return ''; } // Start content generation $content = ''; $attributes = $chelper->get_and_erase_attributes('course_category_tree clearfix'); $content .= html_writer::start_tag('div', $attributes); if ($coursecat->get_children_count()) { $classes = array( 'collapseexpand', 'aabtn' ); // Check if the category content contains subcategories with children's content loaded. if ($this->categoryexpandedonload) { $classes[] = 'collapse-all'; $linkname = get_string('collapseall'); } else { $linkname = get_string('expandall'); } // Only show the collapse/expand if there are children to expand. $content .= html_writer::start_tag('div', array('class' => 'collapsible-actions')); $content .= html_writer::link('#', $linkname, array('class' => implode(' ', $classes))); $content .= html_writer::end_tag('div'); $this->page->requires->strings_for_js(array('collapseall', 'expandall'), 'moodle'); } $content .= html_writer::tag('div', $categorycontent, array('class' => 'content')); $content .= html_writer::end_tag('div'); // .course_category_tree return $content; } /** * Renders HTML to display particular course category - list of it's subcategories and courses * * Invoked from /course/index.php * * @param int|stdClass|core_course_category $category */ public function course_category($category) { global $CFG; $usertop = core_course_category::user_top(); if (empty($category)) { $coursecat = $usertop; } else if (is_object($category) && $category instanceof core_course_category) { $coursecat = $category; } else { $coursecat = core_course_category::get(is_object($category) ? $category->id : $category); } $site = get_site(); $actionbar = new \core_course\output\category_action_bar($this->page, $coursecat); $output = $this->render_from_template('core_course/category_actionbar', $actionbar->export_for_template($this)); if (core_course_category::is_simple_site()) { // There is only one category in the system, do not display link to it. $strfulllistofcourses = get_string('fulllistofcourses'); $this->page->set_title($strfulllistofcourses); } else if (!$coursecat->id || !$coursecat->is_uservisible()) { $strcategories = get_string('categories'); $this->page->set_title($strcategories); } else { $strfulllistofcourses = get_string('fulllistofcourses'); $this->page->set_title($strfulllistofcourses); } // Print current category description $chelper = new coursecat_helper(); if ($description = $chelper->get_category_formatted_description($coursecat)) { $output .= $this->box($description, array('class' => 'generalbox info')); } // Prepare parameters for courses and categories lists in the tree $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_AUTO) ->set_attributes(array('class' => 'category-browse category-browse-'.$coursecat->id)); $coursedisplayoptions = array(); $catdisplayoptions = array(); $browse = optional_param('browse', null, PARAM_ALPHA); $perpage = optional_param('perpage', $CFG->coursesperpage, PARAM_INT); $page = optional_param('page', 0, PARAM_INT); $baseurl = new moodle_url('/course/index.php'); if ($coursecat->id) { $baseurl->param('categoryid', $coursecat->id); } if ($perpage != $CFG->coursesperpage) { $baseurl->param('perpage', $perpage); } $coursedisplayoptions['limit'] = $perpage; $catdisplayoptions['limit'] = $perpage; if ($browse === 'courses' || !$coursecat->get_children_count()) { $coursedisplayoptions['offset'] = $page * $perpage; $coursedisplayoptions['paginationurl'] = new moodle_url($baseurl, array('browse' => 'courses')); $catdisplayoptions['nodisplay'] = true; $catdisplayoptions['viewmoreurl'] = new moodle_url($baseurl, array('browse' => 'categories')); $catdisplayoptions['viewmoretext'] = new lang_string('viewallsubcategories'); } else if ($browse === 'categories' || !$coursecat->get_courses_count()) { $coursedisplayoptions['nodisplay'] = true; $catdisplayoptions['offset'] = $page * $perpage; $catdisplayoptions['paginationurl'] = new moodle_url($baseurl, array('browse' => 'categories')); $coursedisplayoptions['viewmoreurl'] = new moodle_url($baseurl, array('browse' => 'courses')); $coursedisplayoptions['viewmoretext'] = new lang_string('viewallcourses'); } else { // we have a category that has both subcategories and courses, display pagination separately $coursedisplayoptions['viewmoreurl'] = new moodle_url($baseurl, array('browse' => 'courses', 'page' => 1)); $catdisplayoptions['viewmoreurl'] = new moodle_url($baseurl, array('browse' => 'categories', 'page' => 1)); } $chelper->set_courses_display_options($coursedisplayoptions)->set_categories_display_options($catdisplayoptions); // Display course category tree. $output .= $this->coursecat_tree($chelper, $coursecat); return $output; } /** * Serves requests to /course/category.ajax.php * * In this renderer implementation it may expand the category content or * course content. * * @return string * @throws coding_exception */ public function coursecat_ajax() { global $DB, $CFG; $type = required_param('type', PARAM_INT); if ($type === self::COURSECAT_TYPE_CATEGORY) { // This is a request for a category list of some kind. $categoryid = required_param('categoryid', PARAM_INT); $showcourses = required_param('showcourses', PARAM_INT); $depth = required_param('depth', PARAM_INT); $category = core_course_category::get($categoryid); $chelper = new coursecat_helper(); $baseurl = new moodle_url('/course/index.php', array('categoryid' => $categoryid)); $coursedisplayoptions = array( 'limit' => $CFG->coursesperpage, 'viewmoreurl' => new moodle_url($baseurl, array('browse' => 'courses', 'page' => 1)) ); $catdisplayoptions = array( 'limit' => $CFG->coursesperpage, 'viewmoreurl' => new moodle_url($baseurl, array('browse' => 'categories', 'page' => 1)) ); $chelper->set_show_courses($showcourses)-> set_courses_display_options($coursedisplayoptions)-> set_categories_display_options($catdisplayoptions); return $this->coursecat_category_content($chelper, $category, $depth); } else if ($type === self::COURSECAT_TYPE_COURSE) { // This is a request for the course information. $courseid = required_param('courseid', PARAM_INT); $course = $DB->get_record('course', ['id' => $courseid], '*', IGNORE_MISSING); if ($course === false) { throw new \moodle_exception('invalidcourseid'); } $coursecontext = context_course::instance($course->id, MUST_EXIST); if ($course->visible == 0 && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { throw new \moodle_exception('invalidcourseid'); } $chelper = new coursecat_helper(); $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_EXPANDED); return $this->coursecat_coursebox_content($chelper, $course); } else { throw new coding_exception('Invalid request type'); } } /** * Renders html to display search result page * * @param array $searchcriteria may contain elements: search, blocklist, modulelist, tagid * @return string */ public function search_courses($searchcriteria) { global $CFG; $content = ''; $search = ''; if (!empty($searchcriteria['search'])) { $search = $searchcriteria['search']; } $content .= $this->course_search_form($search); if (!empty($searchcriteria)) { // print search results $displayoptions = array('sort' => array('displayname' => 1)); // take the current page and number of results per page from query $perpage = optional_param('perpage', 0, PARAM_RAW); if ($perpage !== 'all') { $displayoptions['limit'] = ((int)$perpage <= 0) ? $CFG->coursesperpage : (int)$perpage; $page = optional_param('page', 0, PARAM_INT); $displayoptions['offset'] = $displayoptions['limit'] * $page; } // options 'paginationurl' and 'paginationallowall' are only used in method coursecat_courses() $displayoptions['paginationurl'] = new moodle_url('/course/search.php', $searchcriteria); $displayoptions['paginationallowall'] = true; // allow adding link 'View all' $class = 'course-search-result'; foreach ($searchcriteria as $key => $value) { if (!empty($value)) { $class .= ' course-search-result-'. $key; } } $chelper = new coursecat_helper(); $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_EXPANDED_WITH_CAT)-> set_courses_display_options($displayoptions)-> set_search_criteria($searchcriteria)-> set_attributes(array('class' => $class)); $courses = core_course_category::search_courses($searchcriteria, $chelper->get_courses_display_options()); $totalcount = core_course_category::search_courses_count($searchcriteria); $courseslist = $this->coursecat_courses($chelper, $courses, $totalcount); if (!$totalcount) { if (!empty($searchcriteria['search'])) { $content .= $this->heading(get_string('nocoursesfound', '', $searchcriteria['search'])); } else { $content .= $this->heading(get_string('novalidcourses')); } } else { $content .= $this->heading(get_string('searchresults'). ": $totalcount"); $content .= $courseslist; } } return $content; } /** * Renders html to print list of courses tagged with particular tag * * @param int $tagid id of the tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param array $displayoptions * @return string empty string if no courses are marked with this tag or rendered list of courses */ public function tagged_courses($tagid, $exclusivemode = true, $ctx = 0, $rec = true, $displayoptions = null) { global $CFG; if (empty($displayoptions)) { $displayoptions = array(); } $showcategories = !core_course_category::is_simple_site(); $displayoptions += array('limit' => $CFG->coursesperpage, 'offset' => 0); $chelper = new coursecat_helper(); $searchcriteria = array('tagid' => $tagid, 'ctx' => $ctx, 'rec' => $rec); $chelper->set_show_courses($showcategories ? self::COURSECAT_SHOW_COURSES_EXPANDED_WITH_CAT : self::COURSECAT_SHOW_COURSES_EXPANDED)-> set_search_criteria($searchcriteria)-> set_courses_display_options($displayoptions)-> set_attributes(array('class' => 'course-search-result course-search-result-tagid')); // (we set the same css class as in search results by tagid) if ($totalcount = core_course_category::search_courses_count($searchcriteria)) { $courses = core_course_category::search_courses($searchcriteria, $chelper->get_courses_display_options()); if ($exclusivemode) { return $this->coursecat_courses($chelper, $courses, $totalcount); } else { $tagfeed = new core_tag\output\tagfeed(); $img = $this->output->pix_icon('i/course', ''); foreach ($courses as $course) { $url = course_get_url($course); $imgwithlink = html_writer::link($url, $img); $coursename = html_writer::link($url, $course->get_formatted_name()); $details = ''; if ($showcategories && ($cat = core_course_category::get($course->category, IGNORE_MISSING))) { $details = get_string('category').': '. html_writer::link(new moodle_url('/course/index.php', array('categoryid' => $cat->id)), $cat->get_formatted_name(), array('class' => $cat->visible ? '' : 'dimmed')); } $tagfeed->add($imgwithlink, $coursename, $details); } return $this->output->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($this->output)); } } return ''; } /** * Returns HTML to display one remote course * * @param stdClass $course remote course information, contains properties: id, remoteid, shortname, fullname, hostid, summary, summaryformat, cat_name, hostname * @return string */ protected function frontpage_remote_course(stdClass $course) { $url = new moodle_url('/auth/mnet/jump.php', array( 'hostid' => $course->hostid, 'wantsurl' => '/course/view.php?id='. $course->remoteid )); $output = ''; $output .= html_writer::start_tag('div', array('class' => 'coursebox remotecoursebox clearfix')); $output .= html_writer::start_tag('div', array('class' => 'info')); $output .= html_writer::start_tag('h3', array('class' => 'coursename')); $output .= html_writer::link($url, format_string($course->fullname), array('title' => get_string('entercourse'))); $output .= html_writer::end_tag('h3'); // .name $output .= html_writer::tag('div', '', array('class' => 'moreinfo')); $output .= html_writer::end_tag('div'); // .info $output .= html_writer::start_tag('div', array('class' => 'content')); $output .= html_writer::start_tag('div', array('class' => 'summary')); $options = new stdClass(); $options->noclean = true; $options->para = false; $options->overflowdiv = true; $output .= format_text($course->summary, $course->summaryformat, $options); $output .= html_writer::end_tag('div'); // .summary $addinfo = format_string($course->hostname) . ' : ' . format_string($course->cat_name) . ' : ' . format_string($course->shortname); $output .= html_writer::tag('div', $addinfo, array('class' => 'remotecourseinfo')); $output .= html_writer::end_tag('div'); // .content $output .= html_writer::end_tag('div'); // .coursebox return $output; } /** * Returns HTML to display one remote host * * @param array $host host information, contains properties: name, url, count * @return string */ protected function frontpage_remote_host($host) { $output = ''; $output .= html_writer::start_tag('div', array('class' => 'coursebox remotehost clearfix')); $output .= html_writer::start_tag('div', array('class' => 'info')); $output .= html_writer::start_tag('h3', array('class' => 'name')); $output .= html_writer::link($host['url'], s($host['name']), array('title' => s($host['name']))); $output .= html_writer::end_tag('h3'); // .name $output .= html_writer::tag('div', '', array('class' => 'moreinfo')); $output .= html_writer::end_tag('div'); // .info $output .= html_writer::start_tag('div', array('class' => 'content')); $output .= html_writer::start_tag('div', array('class' => 'summary')); $output .= $host['count'] . ' ' . get_string('courses'); $output .= html_writer::end_tag('div'); // .content $output .= html_writer::end_tag('div'); // .coursebox return $output; } /** * Returns HTML to print list of courses user is enrolled to for the frontpage * * Also lists remote courses or remote hosts if MNET authorisation is used * * @return string */ public function frontpage_my_courses() { global $USER, $CFG, $DB; if (!isloggedin() or isguestuser()) { return ''; } $output = ''; $courses = enrol_get_my_courses('summary, summaryformat'); $rhosts = array(); $rcourses = array(); if (!empty($CFG->mnet_dispatcher_mode) && $CFG->mnet_dispatcher_mode==='strict') { $rcourses = get_my_remotecourses($USER->id); $rhosts = get_my_remotehosts(); } if (!empty($courses) || !empty($rcourses) || !empty($rhosts)) { $chelper = new coursecat_helper(); $totalcount = count($courses); if (count($courses) > $CFG->frontpagecourselimit) { // There are more enrolled courses than we can display, display link to 'My courses'. $courses = array_slice($courses, 0, $CFG->frontpagecourselimit, true); $chelper->set_courses_display_options(array( 'viewmoreurl' => new moodle_url('/my/courses.php'), 'viewmoretext' => new lang_string('mycourses') )); } else if (core_course_category::top()->is_uservisible()) { // All enrolled courses are displayed, display link to 'All courses' if there are more courses in system. $chelper->set_courses_display_options(array( 'viewmoreurl' => new moodle_url('/course/index.php'), 'viewmoretext' => new lang_string('fulllistofcourses') )); $totalcount = $DB->count_records('course') - 1; } $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_EXPANDED)-> set_attributes(array('class' => 'frontpage-course-list-enrolled')); $output .= $this->coursecat_courses($chelper, $courses, $totalcount); // MNET if (!empty($rcourses)) { // at the IDP, we know of all the remote courses $output .= html_writer::start_tag('div', array('class' => 'courses')); foreach ($rcourses as $course) { $output .= $this->frontpage_remote_course($course); } $output .= html_writer::end_tag('div'); // .courses } elseif (!empty($rhosts)) { // non-IDP, we know of all the remote servers, but not courses $output .= html_writer::start_tag('div', array('class' => 'courses')); foreach ($rhosts as $host) { $output .= $this->frontpage_remote_host($host); } $output .= html_writer::end_tag('div'); // .courses } } return $output; } /** * Returns HTML to print list of available courses for the frontpage * * @return string */ public function frontpage_available_courses() { global $CFG; $chelper = new coursecat_helper(); $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_EXPANDED)-> set_courses_display_options(array( 'recursive' => true, 'limit' => $CFG->frontpagecourselimit, 'viewmoreurl' => new moodle_url('/course/index.php'), 'viewmoretext' => new lang_string('fulllistofcourses'))); $chelper->set_attributes(array('class' => 'frontpage-course-list-all')); $courses = core_course_category::top()->get_courses($chelper->get_courses_display_options()); $totalcount = core_course_category::top()->get_courses_count($chelper->get_courses_display_options()); if (!$totalcount && !$this->page->user_is_editing() && has_capability('moodle/course:create', context_system::instance())) { // Print link to create a new course, for the 1st available category. return $this->add_new_course_button(); } return $this->coursecat_courses($chelper, $courses, $totalcount); } /** * Returns HTML to the "add new course" button for the page * * @return string */ public function add_new_course_button() { global $CFG; // Print link to create a new course, for the 1st available category. $output = $this->container_start('buttons'); $url = new moodle_url('/course/edit.php', array('category' => $CFG->defaultrequestcategory, 'returnto' => 'topcat')); $output .= $this->single_button($url, get_string('addnewcourse'), 'get'); $output .= $this->container_end('buttons'); return $output; } /** * Returns HTML to print tree with course categories and courses for the frontpage * * @return string */ public function frontpage_combo_list() { global $CFG; // TODO MDL-10965 improve. $tree = core_course_category::top(); if (!$tree->get_children_count()) { return ''; } $chelper = new coursecat_helper(); $chelper->set_subcat_depth($CFG->maxcategorydepth)-> set_categories_display_options(array( 'limit' => $CFG->coursesperpage, 'viewmoreurl' => new moodle_url('/course/index.php', array('browse' => 'categories', 'page' => 1)) ))-> set_courses_display_options(array( 'limit' => $CFG->coursesperpage, 'viewmoreurl' => new moodle_url('/course/index.php', array('browse' => 'courses', 'page' => 1)) ))-> set_attributes(array('class' => 'frontpage-category-combo')); return $this->coursecat_tree($chelper, $tree); } /** * Returns HTML to print tree of course categories (with number of courses) for the frontpage * * @return string */ public function frontpage_categories_list() { global $CFG; // TODO MDL-10965 improve. $tree = core_course_category::top(); if (!$tree->get_children_count()) { return ''; } $chelper = new coursecat_helper(); $chelper->set_subcat_depth($CFG->maxcategorydepth)-> set_show_courses(self::COURSECAT_SHOW_COURSES_COUNT)-> set_categories_display_options(array( 'limit' => $CFG->coursesperpage, 'viewmoreurl' => new moodle_url('/course/index.php', array('browse' => 'categories', 'page' => 1)) ))-> set_attributes(array('class' => 'frontpage-category-names')); return $this->coursecat_tree($chelper, $tree); } /** * @deprecated since Moodle 4.3 MDL-78744 */ #[\core\attribute\deprecated(null, since: '4.3', mdl: 'MDL-78744', final: true)] public function render_activity_information() { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } /** * Renders the activity navigation. * * Defer to template. * * @param \core_course\output\activity_navigation $page * @return string html for the page */ public function render_activity_navigation(\core_course\output\activity_navigation $page) { $data = $page->export_for_template($this->output); return $this->output->render_from_template('core_course/activity_navigation', $data); } /** * Display waiting information about backup size during uploading backup process * @param object $backupfile the backup stored_file * @return $html string */ public function sendingbackupinfo($backupfile) { $sizeinfo = new stdClass(); $sizeinfo->total = number_format($backupfile->get_filesize() / 1000000, 2); $html = html_writer::tag('div', get_string('sendingsize', 'hub', $sizeinfo), array('class' => 'courseuploadtextinfo')); return $html; } /** * Hub information (logo - name - description - link) * @param object $hubinfo * @return string html code */ public function hubinfo($hubinfo) { $screenshothtml = html_writer::empty_tag('img', array('src' => $hubinfo['imgurl'], 'alt' => $hubinfo['name'])); $hubdescription = html_writer::tag('div', $screenshothtml, array('class' => 'hubscreenshot')); $hubdescription .= html_writer::tag('a', $hubinfo['name'], array('class' => 'hublink', 'href' => $hubinfo['url'], 'onclick' => 'this.target="_blank"')); $hubdescription .= html_writer::tag('div', format_text($hubinfo['description'], FORMAT_PLAIN), array('class' => 'hubdescription')); $hubdescription = html_writer::tag('div', $hubdescription, array('class' => 'hubinfo clearfix')); return $hubdescription; } /** * Output frontpage summary text and frontpage modules (stored as section 1 in site course) * * This may be disabled in settings * * @return string */ public function frontpage_section1() { global $SITE, $USER; $output = ''; $editing = $this->page->user_is_editing(); if ($editing) { // Make sure section with number 1 exists. course_create_sections_if_missing($SITE, 1); } $modinfo = get_fast_modinfo($SITE); $section = $modinfo->get_section_info(1); if (($section && (!empty($modinfo->sections[1]) or !empty($section->summary))) or $editing) { $format = course_get_format($SITE); $frontpageclass = $format->get_output_classname('content\\frontpagesection'); $frontpagesection = new $frontpageclass($format, $section); // The course outputs works with format renderers, not with course renderers. $renderer = $format->get_renderer($this->page); $output .= $renderer->render($frontpagesection); } return $output; } /** * Output news for the frontpage (extract from site-wide news forum) * * @param stdClass $forum record from db table 'forum' that represents the site news forum * @return string */ protected function frontpage_news($forum) { global $CFG, $SITE, $SESSION, $USER; require_once($CFG->dirroot .'/mod/forum/lib.php'); $output = ''; if (isloggedin()) { $SESSION->fromdiscussion = $CFG->wwwroot; $subtext = ''; if (\mod_forum\subscriptions::is_subscribed($USER->id, $forum)) { if (!\mod_forum\subscriptions::is_forcesubscribed($forum)) { $subtext = get_string('unsubscribe', 'forum'); } } else { $subtext = get_string('subscribe', 'forum'); } $suburl = new moodle_url('/mod/forum/subscribe.php', array('id' => $forum->id, 'sesskey' => sesskey())); $output .= html_writer::tag('div', html_writer::link($suburl, $subtext), ['class' => 'subscribelink text-end']); } $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = context_module::instance($coursemodule->id); $entityfactory = mod_forum\local\container::get_entity_factory(); $forumentity = $entityfactory->get_forum_from_stdclass($forum, $context, $coursemodule, $SITE); $rendererfactory = mod_forum\local\container::get_renderer_factory(); $discussionsrenderer = $rendererfactory->get_frontpage_news_discussion_list_renderer($forumentity); $cm = \cm_info::create($coursemodule); return $output . $discussionsrenderer->render($USER, $cm, null, null, 0, $SITE->newsitems); } /** * Renders part of frontpage with a skip link (i.e. "My courses", "Site news", etc.) * * @param string $skipdivid * @param string $contentsdivid * @param string $header Header of the part * @param string $contents Contents of the part * @return string */ protected function frontpage_part($skipdivid, $contentsdivid, $header, $contents) { if (strval($contents) === '') { return ''; } $output = html_writer::link('#' . $skipdivid, get_string('skipa', 'access', core_text::strtolower(strip_tags($header))), array('class' => 'skip-block skip aabtn')); // Wrap frontpage part in div container. $output .= html_writer::start_tag('div', array('id' => $contentsdivid)); $output .= $this->heading($header); $output .= $contents; // End frontpage part div container. $output .= html_writer::end_tag('div'); $output .= html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => $skipdivid)); return $output; } /** * Outputs contents for frontpage as configured in $CFG->frontpage or $CFG->frontpageloggedin * * @return string */ public function frontpage() { global $CFG, $SITE; $output = ''; if (isloggedin() and !isguestuser() and isset($CFG->frontpageloggedin)) { $frontpagelayout = $CFG->frontpageloggedin; } else { $frontpagelayout = $CFG->frontpage; } foreach (explode(',', $frontpagelayout) as $v) { switch ($v) { // Display the main part of the front page. case FRONTPAGENEWS: if ($SITE->newsitems) { // Print forums only when needed. require_once($CFG->dirroot .'/mod/forum/lib.php'); if (($newsforum = forum_get_course_forum($SITE->id, 'news')) && ($forumcontents = $this->frontpage_news($newsforum))) { $newsforumcm = get_fast_modinfo($SITE)->instances['forum'][$newsforum->id]; $output .= $this->frontpage_part('skipsitenews', 'site-news-forum', $newsforumcm->get_formatted_name(), $forumcontents); } } break; case FRONTPAGEENROLLEDCOURSELIST: $mycourseshtml = $this->frontpage_my_courses(); if (!empty($mycourseshtml)) { $output .= $this->frontpage_part('skipmycourses', 'frontpage-course-list', get_string('mycourses'), $mycourseshtml); } break; case FRONTPAGEALLCOURSELIST: $availablecourseshtml = $this->frontpage_available_courses(); $output .= $this->frontpage_part('skipavailablecourses', 'frontpage-available-course-list', get_string('availablecourses'), $availablecourseshtml); break; case FRONTPAGECATEGORYNAMES: $output .= $this->frontpage_part('skipcategories', 'frontpage-category-names', get_string('categories'), $this->frontpage_categories_list()); break; case FRONTPAGECATEGORYCOMBO: $output .= $this->frontpage_part('skipcourses', 'frontpage-category-combo', get_string('courses'), $this->frontpage_combo_list()); break; case FRONTPAGECOURSESEARCH: $output .= $this->box($this->course_search_form(''), 'd-flex justify-content-center'); break; } $output .= '
'; } return $output; } } /** * Class storing display options and functions to help display course category and/or courses lists * * This is a wrapper for core_course_category objects that also stores display options * and functions to retrieve sorted and paginated lists of categories/courses. * * If theme overrides methods in core_course_renderers that access this class * it may as well not use this class at all or extend it. * * @package core * @copyright 2013 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class coursecat_helper { /** @var string [none, collapsed, expanded] how (if) display courses list */ protected $showcourses = 10; /* core_course_renderer::COURSECAT_SHOW_COURSES_COLLAPSED */ /** @var int depth to expand subcategories in the tree (deeper subcategories will be loaded by AJAX or proceed to category page by clicking on category name) */ protected $subcatdepth = 1; /** @var array options to display courses list */ protected $coursesdisplayoptions = array(); /** @var array options to display subcategories list */ protected $categoriesdisplayoptions = array(); /** @var array additional HTML attributes */ protected $attributes = array(); /** @var array search criteria if the list is a search result */ protected $searchcriteria = null; /** * Sets how (if) to show the courses - none, collapsed, expanded, etc. * * @param int $showcourses SHOW_COURSES_NONE, SHOW_COURSES_COLLAPSED, SHOW_COURSES_EXPANDED, etc. * @return coursecat_helper */ public function set_show_courses($showcourses) { $this->showcourses = $showcourses; // Automatically set the options to preload summary and coursecontacts for core_course_category::get_courses() // and core_course_category::search_courses(). $this->coursesdisplayoptions['summary'] = $showcourses >= core_course_renderer::COURSECAT_SHOW_COURSES_AUTO; $this->coursesdisplayoptions['coursecontacts'] = $showcourses >= core_course_renderer::COURSECAT_SHOW_COURSES_EXPANDED; $this->coursesdisplayoptions['customfields'] = $showcourses >= core_course_renderer::COURSECAT_SHOW_COURSES_COLLAPSED; return $this; } /** * Returns how (if) to show the courses - none, collapsed, expanded, etc. * * @return int - COURSECAT_SHOW_COURSES_NONE, COURSECAT_SHOW_COURSES_COLLAPSED, COURSECAT_SHOW_COURSES_EXPANDED, etc. */ public function get_show_courses() { return $this->showcourses; } /** * Sets the maximum depth to expand subcategories in the tree * * deeper subcategories may be loaded by AJAX or proceed to category page by clicking on category name * * @param int $subcatdepth * @return coursecat_helper */ public function set_subcat_depth($subcatdepth) { $this->subcatdepth = $subcatdepth; return $this; } /** * Returns the maximum depth to expand subcategories in the tree * * deeper subcategories may be loaded by AJAX or proceed to category page by clicking on category name * * @return int */ public function get_subcat_depth() { return $this->subcatdepth; } /** * Sets options to display list of courses * * Options are later submitted as argument to core_course_category::get_courses() and/or core_course_category::search_courses() * * Options that core_course_category::get_courses() accept: * - recursive - return courses from subcategories as well. Use with care, * this may be a huge list! * - summary - preloads fields 'summary' and 'summaryformat' * - coursecontacts - preloads course contacts * - customfields - preloads custom fields data * - isenrolled - preloads indication whether this user is enrolled in the course * - sort - list of fields to sort. Example * array('idnumber' => 1, 'shortname' => 1, 'id' => -1) * will sort by idnumber asc, shortname asc and id desc. * Default: array('sortorder' => 1) * Only cached fields may be used for sorting! * - offset * - limit - maximum number of children to return, 0 or null for no limit * * Options summary and coursecontacts are filled automatically in the set_show_courses() * * Also renderer can set here any additional options it wants to pass between renderer functions. * * @param array $options * @return coursecat_helper */ public function set_courses_display_options($options) { $this->coursesdisplayoptions = $options; $this->set_show_courses($this->showcourses); // this will calculate special display options return $this; } /** * Sets one option to display list of courses * * @see coursecat_helper::set_courses_display_options() * * @param string $key * @param mixed $value * @return coursecat_helper */ public function set_courses_display_option($key, $value) { $this->coursesdisplayoptions[$key] = $value; return $this; } /** * Return the specified option to display list of courses * * @param string $optionname option name * @param mixed $defaultvalue default value for option if it is not specified * @return mixed */ public function get_courses_display_option($optionname, $defaultvalue = null) { if (array_key_exists($optionname, $this->coursesdisplayoptions)) { return $this->coursesdisplayoptions[$optionname]; } else { return $defaultvalue; } } /** * Returns all options to display the courses * * This array is usually passed to {@link core_course_category::get_courses()} or * {@link core_course_category::search_courses()} * * @return array */ public function get_courses_display_options() { return $this->coursesdisplayoptions; } /** * Sets options to display list of subcategories * * Options 'sort', 'offset' and 'limit' are passed to core_course_category::get_children(). * Any other options may be used by renderer functions * * @param array $options * @return coursecat_helper */ public function set_categories_display_options($options) { $this->categoriesdisplayoptions = $options; return $this; } /** * Return the specified option to display list of subcategories * * @param string $optionname option name * @param mixed $defaultvalue default value for option if it is not specified * @return mixed */ public function get_categories_display_option($optionname, $defaultvalue = null) { if (array_key_exists($optionname, $this->categoriesdisplayoptions)) { return $this->categoriesdisplayoptions[$optionname]; } else { return $defaultvalue; } } /** * Returns all options to display list of subcategories * * This array is usually passed to {@link core_course_category::get_children()} * * @return array */ public function get_categories_display_options() { return $this->categoriesdisplayoptions; } /** * Sets additional general options to pass between renderer functions, usually HTML attributes * * @param array $attributes * @return coursecat_helper */ public function set_attributes($attributes) { $this->attributes = $attributes; return $this; } /** * Return all attributes and erases them so they are not applied again * * @param string $classname adds additional class name to the beginning of $attributes['class'] * @return array */ public function get_and_erase_attributes($classname) { $attributes = $this->attributes; $this->attributes = array(); if (empty($attributes['class'])) { $attributes['class'] = ''; } $attributes['class'] = $classname . ' '. $attributes['class']; return $attributes; } /** * Sets the search criteria if the course is a search result * * Search string will be used to highlight terms in course name and description * * @param array $searchcriteria * @return coursecat_helper */ public function set_search_criteria($searchcriteria) { $this->searchcriteria = $searchcriteria; return $this; } /** * Returns formatted and filtered description of the given category * * @param core_course_category $coursecat category * @param stdClass|array $options format options, by default [noclean,overflowdiv], * if context is not specified it will be added automatically * @return string|null */ public function get_category_formatted_description($coursecat, $options = null) { if ($coursecat->id && $coursecat->is_uservisible() && !empty($coursecat->description)) { if (!isset($coursecat->descriptionformat)) { $descriptionformat = FORMAT_MOODLE; } else { $descriptionformat = $coursecat->descriptionformat; } if ($options === null) { $options = array('noclean' => true, 'overflowdiv' => true); } else { $options = (array)$options; } $context = context_coursecat::instance($coursecat->id); if (!isset($options['context'])) { $options['context'] = $context; } $text = file_rewrite_pluginfile_urls($coursecat->description, 'pluginfile.php', $context->id, 'coursecat', 'description', null); return format_text($text, $descriptionformat, $options); } return null; } /** * Returns given course's summary with proper embedded files urls and formatted * * @param core_course_list_element $course * @param array|stdClass $options additional formatting options * @return string */ public function get_course_formatted_summary($course, $options = array()) { global $CFG; require_once($CFG->libdir. '/filelib.php'); if (!$course->has_summary()) { return ''; } $options = (array)$options; $context = context_course::instance($course->id); if (!isset($options['context'])) { $options['context'] = $context; } $summary = file_rewrite_pluginfile_urls($course->summary, 'pluginfile.php', $context->id, 'course', 'summary', null); $summary = format_text($summary, $course->summaryformat, $options); if (!empty($this->searchcriteria['search'])) { $summary = highlight($this->searchcriteria['search'], $summary); } return $summary; } /** * Returns course name as it is configured to appear in courses lists formatted to course context * * @param core_course_list_element $course * @param array|stdClass $options additional formatting options * @return string */ public function get_course_formatted_name($course, $options = array()) { $options = (array)$options; if (!isset($options['context'])) { $options['context'] = context_course::instance($course->id); } $name = format_string(get_course_display_name_for_list($course), true, $options); if (!empty($this->searchcriteria['search'])) { $name = highlight($this->searchcriteria['search'], $name); } return $name; } } PK$9\C\8&(&(reset_form.phpnu[. defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/formslib.php'); require_once($CFG->dirroot . '/course/lib.php'); /** * Defines the course reset settings form. * * @package core_course * @copyright 2007 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_reset_form extends moodleform { /** * Form definition. */ public function definition() { global $CFG, $COURSE, $DB; $mform = $this->_form; $mform->addElement('header', 'generalheader', get_string('general')); $mform->addElement('date_time_selector', 'reset_start_date', get_string('startdate'), ['optional' => true]); $mform->addHelpButton('reset_start_date', 'startdate'); $mform->addElement('date_time_selector', 'reset_end_date', get_string('enddate'), ['optional' => true]); $mform->addHelpButton('reset_end_date', 'enddate'); $mform->addElement('static', 'generaldelete', get_string('delete')); $mform->addElement('checkbox', 'reset_events', get_string('deleteevents', 'calendar')); $mform->addElement('checkbox', 'reset_notes', get_string('deletenotes', 'notes')); $mform->addElement('checkbox', 'reset_comments', get_string('deleteallcomments', 'moodle')); $mform->addElement('checkbox', 'reset_completion', get_string('deletecompletiondata', 'completion')); $mform->addElement('checkbox', 'delete_blog_associations', get_string('deleteblogassociations', 'blog')); $mform->addHelpButton('delete_blog_associations', 'deleteblogassociations', 'blog'); $mform->addElement('checkbox', 'reset_competency_ratings', get_string('deletecompetencyratings', 'core_competency')); $mform->addElement('header', 'rolesheader', get_string('roles')); $roles = get_assignable_roles(context_course::instance($COURSE->id)); $roles[0] = get_string('noroles', 'role'); $roles = array_reverse($roles, true); $attributes = [ 'multiple' => 1, 'size' => min(count($roles), 10), ]; $mform->addElement('select', 'unenrol_users', get_string('unenrolroleusers', 'enrol'), $roles, $attributes); $mform->addElement('static', 'rolesdelete', get_string('delete')); $mform->addElement('checkbox', 'reset_roles_local', get_string('deletelocalroles', 'role')); $mform->addElement('checkbox', 'reset_roles_overrides', get_string('deletecourseoverrides', 'role')); $mform->addElement('header', 'gradebookheader', get_string('gradebook', 'grades')); $mform->addElement('static', 'gradebookdelete', get_string('delete')); $mform->addElement('checkbox', 'reset_gradebook_items', get_string('removeallcourseitems', 'grades')); $mform->addHelpButton('reset_gradebook_items', 'removeallcourseitems', 'grades'); $mform->addElement('checkbox', 'reset_gradebook_grades', get_string('removeallcoursegrades', 'grades')); $mform->addHelpButton('reset_gradebook_grades', 'removeallcoursegrades', 'grades'); $mform->disabledIf('reset_gradebook_grades', 'reset_gradebook_items', 'checked'); $mform->addElement('header', 'groupheader', get_string('groups')); $mform->addElement('static', 'groupdelete', get_string('delete')); $mform->addElement('checkbox', 'reset_groups_remove', get_string('deleteallgroups', 'group')); $mform->addElement('checkbox', 'reset_groups_members', get_string('removegroupsmembers', 'group')); $mform->disabledIf('reset_groups_members', 'reset_groups_remove', 'checked'); $mform->addElement('checkbox', 'reset_groupings_remove', get_string('deleteallgroupings', 'group')); $mform->addElement('checkbox', 'reset_groupings_members', get_string('removegroupingsmembers', 'group')); $mform->disabledIf('reset_groupings_members', 'reset_groupings_remove', 'checked'); $unsupportedmods = []; if ($allmods = $DB->get_records('modules') ) { foreach ($allmods as $mod) { $modname = $mod->name; $modfile = $CFG->dirroot."/mod/$modname/lib.php"; $modresetcourseformdefinition = $modname.'_reset_course_form_definition'; $modresetuserdata = $modname.'_reset_userdata'; if (file_exists($modfile)) { if (!$DB->count_records($modname, ['course' => $COURSE->id])) { continue; // Skip mods with no instances. } include_once($modfile); if (function_exists($modresetcourseformdefinition)) { $modresetcourseformdefinition($mform); } else if (!function_exists($modresetuserdata)) { $unsupportedmods[] = $mod; } } else { debugging('Missing lib.php in '.$modname.' module'); } } } // Mention unsupported mods. if (!empty($unsupportedmods)) { $mform->addElement('header', 'unsupportedheader', get_string('resetnotimplemented')); $mform->addElement('static', 'unsupportedinfo', get_string('resetnotimplementedinfo')); foreach ($unsupportedmods as $mod) { $mform->addElement('static', 'unsup'.$mod->name, get_string('modulenameplural', $mod->name)); } } $elements = $mform->_elements; foreach ($elements as $element) { if (is_a($element, 'HTML_QuickForm_header')) { $mform->setExpanded($element->getName()); } } $mform->addElement('hidden', 'id', $COURSE->id); $mform->setType('id', PARAM_INT); $buttonarray = []; $buttonarray[] = $mform->createElement( 'submit', 'selectdefault', get_string('selectdefault'), null, null, ['customclassoverride' => 'btn-secondary'], ); $buttonarray[] = $mform->createElement( 'submit', 'deselectall', get_string('deselectall'), null, null, ['customclassoverride' => 'btn-secondary'], ); $mform->addGroup($buttonarray, 'buttonar', '', [' '], false); $mform->addElement( 'submit', 'submitbutton', get_string('resetcourse'), [ 'data-action' => 'resetcourse', 'data-courseid' => $COURSE->id, 'data-coursename' => $COURSE->fullname, ], null, ['customclassoverride' => 'btn-danger'], ); $mform->set_sticky_footer('submitbutton'); $mform->closeHeaderBefore('buttonar'); } /** * Method to load default values for the reset course form. */ public function load_defaults() { global $CFG, $COURSE, $DB; $mform = $this->_form; $defaults = [ 'reset_events' => 1, 'reset_roles_local' => 1, 'reset_gradebook_grades' => 1, 'reset_notes' => 1, ]; // Set student as default in unenrol user list, if role with student archetype exist. if ($studentrole = get_archetype_roles('student')) { $defaults['unenrol_users'] = array_keys($studentrole); } if ($allmods = $DB->get_records('modules') ) { foreach ($allmods as $mod) { $modname = $mod->name; $modfile = $CFG->dirroot."/mod/$modname/lib.php"; $modresetcourseformdefaults = $modname.'_reset_course_form_defaults'; if (file_exists($modfile)) { @include_once($modfile); if (function_exists($modresetcourseformdefaults)) { if ($moddefs = $modresetcourseformdefaults($COURSE)) { $defaults = $defaults + $moddefs; } } } } } foreach ($defaults as $element => $default) { $mform->setDefault($element, $default); } } /** * Validation. * * @param array $data * @param array $files * @return array the errors that were found */ public function validation($data, $files) { $course = get_course($data['id']); $errors = parent::validation($data, $files); // We check the values that would be used as start and end. if ($data['reset_start_date'] != 0) { $coursedata['startdate'] = $data['reset_start_date']; } else { $coursedata['startdate'] = $course->startdate; } if ($data['reset_end_date'] != 0) { // End date set by the user has preference. $coursedata['enddate'] = $data['reset_end_date']; } else if ($data['reset_start_date'] > 0 && $course->enddate != 0) { // Otherwise, if the current course enddate is set, reset_course_userdata will add the start date time shift to it. $timeshift = $data['reset_start_date'] - $course->startdate; $coursedata['enddate'] = $course->enddate + $timeshift; } else { $coursedata['enddate'] = $course->enddate; } if ($errorcode = course_validate_dates($coursedata)) { $errors['reset_end_date'] = get_string($errorcode, 'error'); } return $errors; } } PK$9\hAo o info.phpnu[. /** * Displays external information about a course * @package core_course * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../config.php"); require_once("lib.php"); $id = optional_param('id', false, PARAM_INT); // Course id $name = optional_param('name', false, PARAM_RAW); // Course short name if (!$id and !$name) { throw new \moodle_exception("unspecifycourseid"); } if ($name) { if (!$course = $DB->get_record("course", array("shortname"=>$name))) { throw new \moodle_exception("invalidshortname"); } } else { if (!$course = $DB->get_record("course", array("id"=>$id))) { throw new \moodle_exception("invalidcourseid"); } } $site = get_site(); if ($CFG->forcelogin) { require_login(); } $context = context_course::instance($course->id); if (!core_course_category::can_view_course_info($course) && !is_enrolled($context, null, '', true)) { throw new \moodle_exception('cannotviewcategory', '', $CFG->wwwroot .'/'); } $PAGE->set_course($course); $PAGE->set_pagelayout('incourse'); $PAGE->set_url('/course/info.php', array('id' => $course->id)); $strcourseinfo = get_string('courseinfo'); $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, [$strcourseinfo, $course->fullname])); $PAGE->set_heading($strcourseinfo); $PAGE->navbar->add(get_string('summary')); echo $OUTPUT->header(); // print enrol info if ($texts = enrol_get_course_description_texts($course)) { echo $OUTPUT->box_start('generalbox icons'); echo implode($texts); echo $OUTPUT->box_end(); } /** @var core_course_renderer $courserenderer */ $courserenderer = $PAGE->get_renderer('core', 'course'); echo $courserenderer->course_info_box($course); echo "
"; // Trigger event, course information viewed. $eventparams = array('context' => $context, 'objectid' => $course->id); $event = \core\event\course_information_viewed::create($eventparams); $event->trigger(); echo $OUTPUT->footer(); PK$9\P_XE9E9view.phpnu[. /** * Display the course home page. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_course */ require_once('../config.php'); require_once('lib.php'); require_once($CFG->libdir.'/completionlib.php'); redirect_if_major_upgrade_required(); $id = optional_param('id', 0, PARAM_INT); $name = optional_param('name', '', PARAM_TEXT); $edit = optional_param('edit', -1, PARAM_BOOL); $hide = optional_param('hide', 0, PARAM_INT); // TODO remove this param as part of MDL-83530. $show = optional_param('show', 0, PARAM_INT); // TODO remove this param as part of MDL-83530. $duplicatesection = optional_param('duplicatesection', 0, PARAM_INT); $idnumber = optional_param('idnumber', '', PARAM_RAW); $sectionid = optional_param('sectionid', 0, PARAM_INT); $section = optional_param('section', null, PARAM_INT); $expandsection = optional_param('expandsection', -1, PARAM_INT); $move = optional_param('move', 0, PARAM_INT); // TODO remove this param as part of MDL-83530. $marker = optional_param('marker', -1 , PARAM_INT); // TODO remove this param as part of MDL-83530. $switchrole = optional_param('switchrole', -1, PARAM_INT); // Deprecated, use course/switchrole.php instead. $return = optional_param('return', 0, PARAM_LOCALURL); $params = []; if (!empty($name)) { $params = ['shortname' => $name]; } else if (!empty($idnumber)) { $params = ['idnumber' => $idnumber]; } else if (!empty($id)) { $params = ['id' => $id]; } else { throw new \moodle_exception('unspecifycourseid', 'error'); } $course = $DB->get_record('course', $params, '*', MUST_EXIST); $urlparams = ['id' => $course->id]; // Sectionid should get priority over section number. if ($sectionid) { $section = $DB->get_field('course_sections', 'section', ['id' => $sectionid, 'course' => $course->id], MUST_EXIST); } if (!is_null($section)) { $urlparams['section'] = $section; } if ($expandsection !== -1) { $urlparams['expandsection'] = $expandsection; } $PAGE->set_url('/course/view.php', $urlparams); // Defined here to avoid notices on errors etc. // Prevent caching of this page to stop confusion when changing page after making AJAX changes. $PAGE->set_cacheable(false); context_helper::preload_course($course->id); $context = context_course::instance($course->id, MUST_EXIST); // Remove any switched roles before checking login. if ($switchrole == 0 && confirm_sesskey()) { role_switch($switchrole, $context); } require_login($course); // Switchrole - sanity check in cost-order... $resetuserallowedediting = false; if ($switchrole > 0 && confirm_sesskey() && has_capability('moodle/role:switchroles', $context)) { // Is this role assignable in this context? // Inquiring minds want to know. $aroles = get_switchable_roles($context); if (is_array($aroles) && isset($aroles[$switchrole])) { role_switch($switchrole, $context); // Double check that this role is allowed here. require_login($course); } // Reset course page state. This prevents some weird problems. $USER->activitycopy = false; $USER->activitycopycourse = null; unset($USER->activitycopyname); unset($SESSION->modform); $USER->editing = 0; $resetuserallowedediting = true; } // If course is hosted on an external server, redirect to corresponding // url with appropriate authentication attached as parameter. $hook = new \core_course\hook\before_course_viewed($course); \core\hook\manager::get_instance()->dispatch($hook); require_once($CFG->dirroot.'/calendar/lib.php'); // This is after login because it needs $USER. // Must set layout before gettting section info. See MDL-47555. $PAGE->set_pagelayout('course'); $PAGE->add_body_class('limitedwidth'); if ($section && $section > 0) { // Get section details and check it exists. $modinfo = get_fast_modinfo($course); $coursesections = $modinfo->get_section_info($section, MUST_EXIST); // Check user is allowed to see it. if (!$coursesections->uservisible) { // Check if coursesection has conditions affecting availability and if // so, output availability info. if ($coursesections->visible && $coursesections->availableinfo) { $sectionname = get_section_name($course, $coursesections); $message = get_string('notavailablecourse', '', $sectionname); redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR); } else { // Note: We actually already know they don't have this capability // or uservisible would have been true; this is just to get the // correct error message shown. require_capability('moodle/course:viewhiddensections', $context); } } } // Fix course format if it is no longer installed. $format = course_get_format($course); $course->format = $format->get_format(); $PAGE->set_pagetype('course-view-' . $course->format); $PAGE->set_other_editing_capability('moodle/course:update'); $PAGE->set_other_editing_capability('moodle/course:manageactivities'); $PAGE->set_other_editing_capability('moodle/course:activityvisibility'); if (course_format_uses_sections($course->format)) { $PAGE->set_other_editing_capability('moodle/course:sectionvisibility'); $PAGE->set_other_editing_capability('moodle/course:movesections'); } // Preload course format renderer before output starts. // This is a little hacky but necessary since // format.php is not included until after output starts. $renderer = $format->get_renderer($PAGE); if ($resetuserallowedediting) { // Ugly hack. unset($PAGE->_user_allowed_editing); } if (!isset($USER->editing)) { $USER->editing = 0; } if ($PAGE->user_allowed_editing()) { if (($edit == 1) && confirm_sesskey()) { $USER->editing = 1; // Redirect to site root if Editing is toggled on frontpage. if ($course->id == SITEID) { redirect($CFG->wwwroot .'/?redirect=0'); } else if (!empty($return)) { redirect($CFG->wwwroot . $return); } else { $url = new moodle_url($PAGE->url, ['notifyeditingon' => 1]); redirect($url); } } else if (($edit == 0) && confirm_sesskey()) { $USER->editing = 0; if (!empty($USER->activitycopy) && $USER->activitycopycourse == $course->id) { $USER->activitycopy = false; $USER->activitycopycourse = null; } // Redirect to site root if Editing is toggled on frontpage. if ($course->id == SITEID) { redirect($CFG->wwwroot .'/?redirect=0'); } else if (!empty($return)) { redirect($CFG->wwwroot . $return); } else { redirect($PAGE->url); } } // TODO remove this if as part of MDL-83530. if (has_capability('moodle/course:sectionvisibility', $context)) { if ($hide && confirm_sesskey()) { debugging( 'The hide param in course view is deprecated. Please use course/format/update.php instead.', DEBUG_DEVELOPER ); set_section_visible($course->id, $hide, '0'); if ($sectionid) { redirect(course_get_url($course, $section, ['navigation' => true])); } else { redirect($PAGE->url); } } if ($show && confirm_sesskey()) { debugging( 'The show param in course view is deprecated. Please use course/format/update.php instead.', DEBUG_DEVELOPER ); set_section_visible($course->id, $show, '1'); if ($sectionid) { redirect(course_get_url($course, $section, ['navigation' => true])); } else { redirect($PAGE->url); } } } // TODO remove this if as part of MDL-83530. if ($marker >= 0 && confirm_sesskey()) { debugging( 'The marker param in course view is deprecated. Please use course/format/update.php instead.', DEBUG_DEVELOPER ); course_set_marker($course->id, $marker); if ($sectionid) { redirect(course_get_url($course, $section, ['navigation' => true])); } else { redirect($PAGE->url); } } if ( !empty($section) && !empty($coursesections) && !empty($duplicatesection) && has_capability('moodle/course:update', $context) && confirm_sesskey() ) { $newsection = $format->duplicate_section($coursesections); redirect(course_get_url($course, $newsection->section)); } // TODO remove this if as part of MDL-83530. if ( !empty($section) && !empty($move) && has_capability('moodle/course:movesections', $context) && confirm_sesskey() ) { debugging( 'The move param is deprecated. Please use the standard move modal instead.', DEBUG_DEVELOPER ); $destsection = $section + $move; if (move_section_to($course, $section, $destsection)) { if ($course->id == SITEID) { redirect($CFG->wwwroot . '/?redirect=0'); } else { if ($format->get_course_display() == COURSE_DISPLAY_MULTIPAGE) { redirect(course_get_url($course)); } else { redirect(course_get_url($course, $destsection)); } } } else { echo $OUTPUT->notification('An error occurred while moving a section'); } } } else { $USER->editing = 0; } $SESSION->fromdiscussion = $PAGE->url->out(false); if ($course->id == SITEID) { // This course is not a real course. redirect($CFG->wwwroot .'/?redirect=0'); } // Determine whether the user has permission to download course content. $candownloadcourse = \core\content::can_export_context($context, $USER); // We are currently keeping the button here from 1.x to help new teachers figure out // what to do, even though the link also appears in the course admin block. It also // means you can back out of a situation where you removed the admin block. if ($PAGE->user_allowed_editing()) { $buttons = $OUTPUT->edit_button($PAGE->url); $PAGE->set_button($buttons); } $editingtitle = ''; if ($PAGE->user_is_editing()) { // Append this to the page title's lang string to get its equivalent when editing mode is turned on. $editingtitle = 'editing'; } // If viewing a section, make the title more specific. if ($section && $section > 0 && course_format_uses_sections($course->format)) { $sectionname = $format->get_generic_section_name(); $sectiontitle = $format->get_section_name($section); $PAGE->set_title( get_string( 'coursesectiontitle' . $editingtitle, 'moodle', ['course' => $course->fullname, 'sectiontitle' => $sectiontitle, 'sectionname' => $sectionname] ) ); } else { $PAGE->set_title(get_string('coursetitle' . $editingtitle, 'moodle', ['course' => $course->fullname])); } // Add bulk editing control. $bulkbutton = $renderer->bulk_editing_button($format); if (!empty($bulkbutton)) { $PAGE->add_header_action($bulkbutton); } $PAGE->set_heading($course->fullname); // Make sure that section 0 exists (this function will create one if it is missing). course_create_sections_if_missing($course, 0); // Get information about course modules and existing module types. // format.php in course formats may rely on presence of these variables. $modinfo = get_fast_modinfo($course); $modnames = get_module_types_names(); $modnamesplural = get_module_types_names(true); $modnamesused = $modinfo->get_used_module_names(); $mods = $modinfo->get_cms(); $sections = $modinfo->get_section_info_all(); // Include course AJAX. This should be done before starting the UI // to allow page header, blocks, or drawers use the course editor. include_course_ajax($course, $modnamesused); echo $OUTPUT->header(); // Show communication room status notification. if (has_capability('moodle/course:update', $context)) { core_communication\helper::get_course_communication_status_notification($course); } $containerattributes = ['class' => 'course-content']; if ($PAGE->user_is_editing()) { // MDL-65321 The backup libraries are quite heavy, only require the bare minimum. require_once($CFG->dirroot . '/backup/util/helper/async_helper.class.php'); if (async_helper::is_async_pending($id, 'course', 'backup')) { echo $OUTPUT->notification(get_string('pendingasyncedit', 'backup'), 'warning'); } // Allow drag and drop in the course index. $containerattributes['data-courseindexdndallowed'] = 'true'; } // Course wrapper start. echo html_writer::start_tag('div', $containerattributes); // CAUTION, hacky fundamental variable defintion to follow! // Note that because of the way course fromats are constructed though // inclusion we pass parameters around this way. $displaysection = $section; // Include the actual course format. require($CFG->dirroot .'/course/format/'. $course->format .'/format.php'); // Content wrapper end. echo html_writer::end_tag('div'); // Trigger course viewed event. // We don't trust $context here. Course format inclusion above executes in the global space. We can't assume // anything after that point. course_view(context_course::instance($course->id), $section); // If available, include the JS to prepare the download course content modal. if ($candownloadcourse) { $PAGE->requires->js_call_amd('core_course/downloadcontent', 'init'); } // Load the view JS module if completion tracking is enabled for this course. $completion = new completion_info($course); if ($completion->is_enabled()) { $PAGE->requires->js_call_amd('core_course/view', 'init'); } echo $OUTPUT->footer(); PK$9\kk edit_form.phpnu[libdir.'/formslib.php'); require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->libdir . '/pdflib.php'); /** * The form for handling editing a course. */ class course_edit_form extends moodleform { protected $course; protected $context; /** * Form definition. */ function definition() { global $CFG, $PAGE; $mform = $this->_form; $PAGE->requires->js_call_amd('core_course/formatchooser', 'init'); $course = $this->_customdata['course']; // this contains the data of this form $category = $this->_customdata['category']; $editoroptions = $this->_customdata['editoroptions']; $returnto = $this->_customdata['returnto']; $returnurl = $this->_customdata['returnurl']; $systemcontext = context_system::instance(); $categorycontext = context_coursecat::instance($category->id); if (!empty($course->id)) { $coursecontext = context_course::instance($course->id); $context = $coursecontext; } else { $coursecontext = null; $context = $categorycontext; } $courseconfig = get_config('moodlecourse'); $this->course = $course; $this->context = $context; // Form definition with new course defaults. $mform->addElement('header','general', get_string('general', 'form')); $mform->addElement('hidden', 'returnto', null); $mform->setType('returnto', PARAM_ALPHANUM); $mform->setConstant('returnto', $returnto); $mform->addElement('hidden', 'returnurl', null); $mform->setType('returnurl', PARAM_LOCALURL); $mform->setConstant('returnurl', $returnurl); $mform->addElement('text', 'fullname', get_string('fullnamecourse'), ['maxlength' => \core_course\constants::FULLNAME_MAXIMUM_LENGTH, 'size' => 50]); $mform->addHelpButton('fullname', 'fullnamecourse'); $mform->addRule('fullname', get_string('missingfullname'), 'required', null, 'client'); $mform->setType('fullname', PARAM_TEXT); if (!empty($course->id) and !has_capability('moodle/course:changefullname', $coursecontext)) { $mform->hardFreeze('fullname'); $mform->setConstant('fullname', $course->fullname); } $mform->addElement('text', 'shortname', get_string('shortnamecourse'), ['maxlength' => \core_course\constants::SHORTNAME_MAXIMUM_LENGTH, 'size' => 20]); $mform->addHelpButton('shortname', 'shortnamecourse'); $mform->addRule('shortname', get_string('missingshortname'), 'required', null, 'client'); $mform->setType('shortname', PARAM_TEXT); if (!empty($course->id) and !has_capability('moodle/course:changeshortname', $coursecontext)) { $mform->hardFreeze('shortname'); $mform->setConstant('shortname', $course->shortname); } // Verify permissions to change course category or keep current. if (empty($course->id)) { if (has_capability('moodle/course:create', $categorycontext)) { $displaylist = core_course_category::make_categories_list('moodle/course:create'); $mform->addElement('autocomplete', 'category', get_string('coursecategory'), $displaylist); $mform->addRule('category', null, 'required', null, 'client'); $mform->addHelpButton('category', 'coursecategory'); $mform->setDefault('category', $category->id); } else { $mform->addElement('hidden', 'category', null); $mform->setType('category', PARAM_INT); $mform->setConstant('category', $category->id); } } else { if (has_capability('moodle/course:changecategory', $coursecontext)) { $displaylist = core_course_category::make_categories_list('moodle/course:changecategory'); if (!isset($displaylist[$course->category])) { //always keep current $displaylist[$course->category] = core_course_category::get($course->category, MUST_EXIST, true) ->get_formatted_name(); } $mform->addElement('autocomplete', 'category', get_string('coursecategory'), $displaylist); $mform->addRule('category', null, 'required', null, 'client'); $mform->addHelpButton('category', 'coursecategory'); } else { //keep current $mform->addElement('hidden', 'category', null); $mform->setType('category', PARAM_INT); $mform->setConstant('category', $course->category); } } $choices = array(); $choices['0'] = get_string('hide'); $choices['1'] = get_string('show'); $mform->addElement('select', 'visible', get_string('coursevisibility'), $choices); $mform->addHelpButton('visible', 'coursevisibility'); $mform->setDefault('visible', $courseconfig->visible); if (!empty($course->id)) { if (!has_capability('moodle/course:visibility', $coursecontext)) { $mform->hardFreeze('visible'); $mform->setConstant('visible', $course->visible); } } else { if (!guess_if_creator_will_have_course_capability('moodle/course:visibility', $categorycontext)) { $mform->hardFreeze('visible'); $mform->setConstant('visible', $courseconfig->visible); } } // Download course content. if ($CFG->downloadcoursecontentallowed) { $downloadchoices = [ DOWNLOAD_COURSE_CONTENT_DISABLED => get_string('no'), DOWNLOAD_COURSE_CONTENT_ENABLED => get_string('yes'), ]; $sitedefaultstring = $downloadchoices[$courseconfig->downloadcontentsitedefault]; $downloadchoices[DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT] = get_string('sitedefaultspecified', '', $sitedefaultstring); $downloadselectdefault = $courseconfig->downloadcontent ?? DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT; $mform->addElement('select', 'downloadcontent', get_string('enabledownloadcoursecontent', 'course'), $downloadchoices); $mform->addHelpButton('downloadcontent', 'downloadcoursecontent', 'course'); $mform->setDefault('downloadcontent', $downloadselectdefault); if ((!empty($course->id) && !has_capability('moodle/course:configuredownloadcontent', $coursecontext)) || (empty($course->id) && !guess_if_creator_will_have_course_capability('moodle/course:configuredownloadcontent', $categorycontext))) { $mform->hardFreeze('downloadcontent'); $mform->setConstant('downloadcontent', $downloadselectdefault); } } // Get the task to change automatically the course visibility when the current day matches the course start date. $task = \core\task\manager::get_scheduled_task('\core\task\show_started_courses_task'); $startdatestring = 'startdate'; if (!empty($task) && !$task->get_disabled()) { // When the task is enabled, display a different help message. $startdatestring = 'startdatewithtaskenabled'; } $mform->addElement('date_time_selector', 'startdate', get_string('startdate')); $mform->addHelpButton('startdate', $startdatestring); $date = (new DateTime())->setTimestamp(usergetmidnight(time())); $date->modify('+1 day'); $mform->setDefault('startdate', $date->getTimestamp()); // Get the task to change automatically the course visibility when the current day matches the course end date. $task = \core\task\manager::get_scheduled_task('\core\task\hide_ended_courses_task'); $enddatestring = 'enddate'; if (!empty($task) && !$task->get_disabled()) { // When the task is enabled, display a different help message. $enddatestring = 'enddatewithtaskenabled'; } $mform->addElement('date_time_selector', 'enddate', get_string('enddate'), array('optional' => true)); $mform->addHelpButton('enddate', $enddatestring); if (!empty($CFG->enablecourserelativedates)) { $attributes = [ 'aria-describedby' => 'relativedatesmode_warning' ]; if (!empty($course->id)) { $attributes['disabled'] = true; } $relativeoptions = [ 0 => get_string('no'), 1 => get_string('yes'), ]; $relativedatesmodegroup = []; $relativedatesmodegroup[] = $mform->createElement('select', 'relativedatesmode', get_string('relativedatesmode'), $relativeoptions, $attributes); $relativedatesmodegroup[] = $mform->createElement('html', html_writer::span(get_string('relativedatesmode_warning'), '', ['id' => 'relativedatesmode_warning'])); $mform->addGroup($relativedatesmodegroup, 'relativedatesmodegroup', get_string('relativedatesmode'), null, false); $mform->addHelpButton('relativedatesmodegroup', 'relativedatesmode'); } $mform->addElement('text','idnumber', get_string('idnumbercourse'),'maxlength="100" size="10"'); $mform->addHelpButton('idnumber', 'idnumbercourse'); $mform->setType('idnumber', PARAM_RAW); if (!empty($course->id) and !has_capability('moodle/course:changeidnumber', $coursecontext)) { $mform->hardFreeze('idnumber'); $mform->setConstants('idnumber', $course->idnumber); } // Description. $mform->addElement('header', 'descriptionhdr', get_string('description')); $mform->setExpanded('descriptionhdr'); $mform->addElement('editor','summary_editor', get_string('coursesummary'), null, $editoroptions); $mform->addHelpButton('summary_editor', 'coursesummary'); $mform->setType('summary_editor', PARAM_RAW); $summaryfields = 'summary_editor'; if ($overviewfilesoptions = course_overviewfiles_options($course)) { $mform->addElement('filemanager', 'overviewfiles_filemanager', get_string('courseoverviewfiles'), null, $overviewfilesoptions); $mform->addHelpButton('overviewfiles_filemanager', 'courseoverviewfiles'); $summaryfields .= ',overviewfiles_filemanager'; } if (!empty($course->id) and !has_capability('moodle/course:changesummary', $coursecontext)) { // Remove the description header it does not contain anything any more. $mform->removeElement('descriptionhdr'); $mform->hardFreeze($summaryfields); } // Course format. $mform->addElement('header', 'courseformathdr', get_string('type_format', 'plugin')); $courseformats = get_sorted_course_formats(true); $formcourseformats = new core\output\choicelist(); $formcourseformats->set_allow_empty(false); foreach ($courseformats as $courseformat) { $definition = []; $component = "format_$courseformat"; if (get_string_manager()->string_exists('plugin_description', $component)) { $definition['description'] = get_string('plugin_description', $component); } $formcourseformats->add_option( $courseformat, get_string('pluginname', "format_$courseformat"), [ 'description' => $definition, ], ); } if (isset($course->format)) { $course->format = course_get_format($course)->get_format(); // Replace with default if not found. if (!in_array($course->format, $courseformats)) { // This format is disabled. Still display it in the dropdown. $formcourseformats->add_option( $course->format, get_string('withdisablednote', 'moodle', get_string('pluginname', 'format_'.$course->format)), ); } } $mform->addElement( 'choicedropdown', 'format', get_string('format'), $formcourseformats, ['data-formatchooser-field' => 'selector'], ); $mform->setDefault('format', $courseconfig->format); // Button to update format-specific options on format change (will be hidden by JavaScript). $mform->registerNoSubmitButton('updatecourseformat'); $mform->addElement('submit', 'updatecourseformat', get_string('courseformatudpate'), [ 'data-formatchooser-field' => 'updateButton', 'class' => 'd-none', ]); // Just a placeholder for the course format options. $mform->addElement('hidden', 'addcourseformatoptionshere'); $mform->setType('addcourseformatoptionshere', PARAM_BOOL); // Appearance. $mform->addElement('header', 'appearancehdr', get_string('appearance')); if (!empty($CFG->allowcoursethemes)) { $themeobjects = get_list_of_themes(); $themes=array(); $themes[''] = get_string('forceno'); foreach ($themeobjects as $key=>$theme) { if (empty($theme->hidefromselector)) { $themes[$key] = get_string('pluginname', 'theme_'.$theme->name); } } $mform->addElement('select', 'theme', get_string('forcetheme'), $themes); } if ((empty($course->id) && guess_if_creator_will_have_course_capability('moodle/course:setforcedlanguage', $categorycontext)) || (!empty($course->id) && has_capability('moodle/course:setforcedlanguage', $coursecontext))) { $languages = ['' => get_string('forceno')]; $languages += get_string_manager()->get_list_of_translations(); $mform->addElement('select', 'lang', get_string('forcelanguage'), $languages); $mform->setDefault('lang', $courseconfig->lang); } // Multi-Calendar Support - see MDL-18375. $calendartypes = \core_calendar\type_factory::get_list_of_calendar_types(); // We do not want to show this option unless there is more than one calendar type to display. if (count($calendartypes) > 1) { $calendars = array(); $calendars[''] = get_string('forceno'); $calendars += $calendartypes; $mform->addElement('select', 'calendartype', get_string('forcecalendartype', 'calendar'), $calendars); } $options = range(0, 10); $mform->addElement('select', 'newsitems', get_string('newsitemsnumber'), $options); $courseconfig = get_config('moodlecourse'); $mform->setDefault('newsitems', $courseconfig->newsitems); $mform->addHelpButton('newsitems', 'newsitemsnumber'); $mform->addElement('selectyesno', 'showgrades', get_string('showgrades')); $mform->addHelpButton('showgrades', 'showgrades'); $mform->setDefault('showgrades', $courseconfig->showgrades); $mform->addElement('selectyesno', 'showreports', get_string('showreports')); $mform->addHelpButton('showreports', 'showreports'); $mform->setDefault('showreports', $courseconfig->showreports); // Show activity dates. $mform->addElement('selectyesno', 'showactivitydates', get_string('showactivitydates')); $mform->addHelpButton('showactivitydates', 'showactivitydates'); $mform->setDefault('showactivitydates', $courseconfig->showactivitydates); // Files and uploads. $mform->addElement('header', 'filehdr', get_string('filesanduploads')); if (!empty($course->legacyfiles) or !empty($CFG->legacyfilesinnewcourses)) { if (empty($course->legacyfiles)) { //0 or missing means no legacy files ever used in this course - new course or nobody turned on legacy files yet $choices = array('0'=>get_string('no'), '2'=>get_string('yes')); } else { $choices = array('1'=>get_string('no'), '2'=>get_string('yes')); } $mform->addElement('select', 'legacyfiles', get_string('courselegacyfiles'), $choices); $mform->addHelpButton('legacyfiles', 'courselegacyfiles'); if (!isset($courseconfig->legacyfiles)) { // in case this was not initialised properly due to switching of $CFG->legacyfilesinnewcourses $courseconfig->legacyfiles = 0; } $mform->setDefault('legacyfiles', $courseconfig->legacyfiles); } // Handle non-existing $course->maxbytes on course creation. $coursemaxbytes = !isset($course->maxbytes) ? null : $course->maxbytes; // Let's prepare the maxbytes popup. $choices = get_max_upload_sizes($CFG->maxbytes, 0, 0, $coursemaxbytes); $mform->addElement('select', 'maxbytes', get_string('maximumupload'), $choices); $mform->addHelpButton('maxbytes', 'maximumupload'); $mform->setDefault('maxbytes', $courseconfig->maxbytes); // PDF font. if (!empty($CFG->enablepdfexportfont)) { $pdf = new \pdf; $fontlist = $pdf->get_export_fontlist(); // Show the option if the font is defined more than one. if (count($fontlist) > 1) { $defaultfont = $courseconfig->pdfexportfont ?? 'freesans'; if (empty($fontlist[$defaultfont])) { $defaultfont = current($fontlist); } $mform->addElement('select', 'pdfexportfont', get_string('pdfexportfont', 'course'), $fontlist); $mform->addHelpButton('pdfexportfont', 'pdfexportfont', 'course'); $mform->setDefault('pdfexportfont', $defaultfont); } } // Completion tracking. if (completion_info::is_enabled_for_site()) { $mform->addElement('header', 'completionhdr', get_string('completion', 'completion')); $mform->addElement('selectyesno', 'enablecompletion', get_string('enablecompletion', 'completion')); $mform->setDefault('enablecompletion', $courseconfig->enablecompletion); $mform->addHelpButton('enablecompletion', 'enablecompletion', 'completion'); $showcompletionconditions = $courseconfig->showcompletionconditions ?? COMPLETION_SHOW_CONDITIONS; $mform->addElement('selectyesno', 'showcompletionconditions', get_string('showcompletionconditions', 'completion')); $mform->addHelpButton('showcompletionconditions', 'showcompletionconditions', 'completion'); $mform->setDefault('showcompletionconditions', $showcompletionconditions); $mform->hideIf('showcompletionconditions', 'enablecompletion', 'eq', COMPLETION_DISABLED); } else { $mform->addElement('hidden', 'enablecompletion'); $mform->setType('enablecompletion', PARAM_INT); $mform->setDefault('enablecompletion', 0); } enrol_course_edit_form($mform, $course, $context); $mform->addElement('header','groups', get_string('groupsettingsheader', 'group')); $choices = array(); $choices[NOGROUPS] = get_string('groupsnone', 'group'); $choices[SEPARATEGROUPS] = get_string('groupsseparate', 'group'); $choices[VISIBLEGROUPS] = get_string('groupsvisible', 'group'); $mform->addElement('select', 'groupmode', get_string('groupmode', 'group'), $choices); $mform->addHelpButton('groupmode', 'groupmode', 'group'); $mform->setDefault('groupmode', $courseconfig->groupmode); $mform->addElement('selectyesno', 'groupmodeforce', get_string('groupmodeforce', 'group')); $mform->addHelpButton('groupmodeforce', 'groupmodeforce', 'group'); $mform->setDefault('groupmodeforce', $courseconfig->groupmodeforce); //default groupings selector $options = array(); $options[0] = get_string('none'); $mform->addElement('select', 'defaultgroupingid', get_string('defaultgrouping', 'group'), $options); if (core_tag_tag::is_enabled('core', 'course') && ((empty($course->id) && guess_if_creator_will_have_course_capability('moodle/course:tag', $categorycontext)) || (!empty($course->id) && has_capability('moodle/course:tag', $coursecontext)))) { $mform->addElement('header', 'tagshdr', get_string('tags', 'tag')); $mform->addElement('tags', 'tags', get_string('tags'), array('itemtype' => 'course', 'component' => 'core')); } // Add custom fields to the form. $handler = core_course\customfield\course_handler::create(); $handler->set_parent_context($categorycontext); // For course handler only. $handler->instance_form_definition($mform, empty($course->id) ? 0 : $course->id); $hook = new \core_course\hook\after_form_definition($this, $mform); di::get(hook\manager::class)->dispatch($hook); // When two elements we need a group. $buttonarray = array(); $classarray = array('class' => 'form-submit'); if (!empty($returnto)) { $buttonarray[] = &$mform->createElement('submit', 'saveandreturn', get_string('savechangesandreturn'), $classarray); } $buttonarray[] = &$mform->createElement('submit', 'saveanddisplay', get_string('savechangesanddisplay'), $classarray); $buttonarray[] = &$mform->createElement('cancel'); $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); $mform->closeHeaderBefore('buttonar'); $mform->addElement('hidden', 'id', null); $mform->setType('id', PARAM_INT); // Communication api call to set the communication data in the form for handling actions for group feature changes. // We only need to set the data for courses already created. if (!empty($course->id)) { $communication = core_communication\helper::load_by_course( courseid: $course->id, context: $coursecontext, ); $communication->set_data($course); } // Prepare custom fields data. $handler->instance_form_before_set_data($course); // Finally set the current form data $this->set_data($course); } /** * Fill in the current page data for this course. */ function definition_after_data() { global $DB; $mform = $this->_form; // add available groupings $courseid = $mform->getElementValue('id'); if ($courseid and $mform->elementExists('defaultgroupingid')) { $options = array(); if ($groupings = $DB->get_records('groupings', array('courseid'=>$courseid))) { foreach ($groupings as $grouping) { $options[$grouping->id] = format_string($grouping->name); } } core_collator::asort($options); $gr_el =& $mform->getElement('defaultgroupingid'); $gr_el->load($options); } // add course format options $formatvalue = $mform->getElementValue('format'); if (is_array($formatvalue) && !empty($formatvalue)) { $params = array('format' => $formatvalue[0]); // Load the course as well if it is available, course formats may need it to work out // they preferred course end date. if ($courseid) { $params['id'] = $courseid; } $courseformat = course_get_format((object)$params); $elements = $courseformat->create_edit_form_elements($mform); for ($i = 0; $i < count($elements); $i++) { $mform->insertElementBefore($mform->removeElement($elements[$i]->getName(), false), 'addcourseformatoptionshere'); } // Remove newsitems element if format does not support news. if (!$courseformat->supports_news()) { $mform->removeElement('newsitems'); } } // Tweak the form with values provided by custom fields in use. $handler = core_course\customfield\course_handler::create(); $handler->instance_form_definition_after_data($mform, empty($courseid) ? 0 : $courseid); $hook = new \core_course\hook\after_form_definition_after_data($this, $mform); di::get(hook\manager::class)->dispatch($hook); } /** * Validation. * * @param array $data * @param array $files * @return array the errors that were found */ function validation($data, $files) { global $DB; $errors = parent::validation($data, $files); // Add field validation check for duplicate shortname. if ($course = $DB->get_record('course', array('shortname' => $data['shortname']), '*', IGNORE_MULTIPLE)) { if (empty($data['id']) || $course->id != $data['id']) { $errors['shortname'] = get_string('shortnametaken', '', $course->fullname); } } // Add field validation check for duplicate idnumber. if (!empty($data['idnumber']) && (empty($data['id']) || $this->course->idnumber != $data['idnumber'])) { if ($course = $DB->get_record('course', array('idnumber' => $data['idnumber']), '*', IGNORE_MULTIPLE)) { if (empty($data['id']) || $course->id != $data['id']) { $errors['idnumber'] = get_string('courseidnumbertaken', 'error', $course->fullname); } } } if ($errorcode = course_validate_dates($data)) { $errors['enddate'] = get_string($errorcode, 'error'); } $errors = array_merge($errors, enrol_course_edit_validation($data, $this->context)); $courseformat = course_get_format((object)array('format' => $data['format'])); $formaterrors = $courseformat->edit_form_validation($data, $files, $errors); if (!empty($formaterrors) && is_array($formaterrors)) { $errors = array_merge($errors, $formaterrors); } // Add the custom fields validation. $handler = core_course\customfield\course_handler::create(); $errors = array_merge($errors, $handler->instance_form_validation($data, $files)); $hook = new \core_course\hook\after_form_validation($this, $data, $files); di::get(hook\manager::class)->dispatch($hook); $pluginerrors = $hook->get_errors(); if (!empty($pluginerrors)) { $errors = array_merge($errors, $pluginerrors); } return $errors; } /** * Returns course object. * * @return \stdClass */ public function get_course(): stdClass { return $this->course; } /** * Returns context. * * @return \core\context */ public function get_context(): \core\context { return $this->context; } } PK$9\}u55customfield.phpnu[. /** * Manage course custom fields * * @package core_course * @copyright 2018 Toni Barbera (toni@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once($CFG->libdir.'/adminlib.php'); admin_externalpage_setup('course_customfield'); $output = $PAGE->get_renderer('core_customfield'); $handler = core_course\customfield\course_handler::create(); $outputpage = new \core_customfield\output\management($handler); echo $output->header(), $output->heading(new lang_string('course_customfield', 'admin')), $output->render($outputpage), $output->footer(); PK$9\jɸ88 delete.phpnu[. /** * Admin-only code to delete a course utterly. * * @package core_course * @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_OUTPUT_BUFFERING', true); require_once(__DIR__ . '/../config.php'); require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); $id = required_param('id', PARAM_INT); // Course ID. $delete = optional_param('delete', '', PARAM_ALPHANUM); // Confirmation hash. $course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); $coursecontext = context_course::instance($course->id); require_login(); if ($SITE->id == $course->id || !can_delete_course($id)) { // Can not delete frontpage or don't have permission to delete the course. throw new \moodle_exception('cannotdeletecourse'); } $categorycontext = context_coursecat::instance($course->category); $PAGE->set_url('/course/delete.php', array('id' => $id)); $PAGE->set_context($categorycontext); $PAGE->set_pagelayout('admin'); navigation_node::override_active_url(new moodle_url('/course/management.php', array('categoryid'=>$course->category))); $courseshortname = format_string($course->shortname, true, array('context' => $coursecontext)); $coursefullname = format_string($course->fullname, true, array('context' => $coursecontext)); $categoryurl = new moodle_url('/course/management.php', array('categoryid' => $course->category)); // Check if we've got confirmation. if ($delete === md5($course->timemodified)) { // We do - time to delete the course. require_sesskey(); $strdeletingcourse = get_string("deletingcourse", "", $courseshortname); $PAGE->navbar->add($strdeletingcourse); $PAGE->set_title($strdeletingcourse); $PAGE->set_heading($SITE->fullname); echo $OUTPUT->header(); echo $OUTPUT->heading($strdeletingcourse); // This might take a while. Raise the execution time limit. core_php_time_limit::raise(); // We do this here because it spits out feedback as it goes. echo $OUTPUT->footer(); echo $OUTPUT->select_element_for_append(); // Preemptively reset the navcache before closing, so it remains the same on shutdown. navigation_cache::destroy_volatile_caches(); \core\session\manager::write_close(); delete_course($course); echo $OUTPUT->heading( get_string("deletedcourse", "", $courseshortname) ); // Update course count in categories. fix_course_sortorder(); echo $OUTPUT->continue_button($categoryurl); exit; // We must exit here!!! } $strdeletecheck = get_string("deletecheck", "", $courseshortname); $PAGE->navbar->add($strdeletecheck); $PAGE->set_title($strdeletecheck); $PAGE->set_heading($SITE->fullname); echo $OUTPUT->header(); // Only let user delete this course if there is not an async backup in progress. if (!async_helper::is_async_pending($id, 'course', 'backup')) { $strdeletecoursecheck = get_string("deletecoursecheck"); $message = "{$strdeletecoursecheck}

{$coursefullname} ({$courseshortname})"; $continueurl = new moodle_url('/course/delete.php', array('id' => $course->id, 'delete' => md5($course->timemodified))); $continuebutton = new single_button($continueurl, get_string('delete'), 'post'); echo $OUTPUT->confirm($message, $continuebutton, $categoryurl); } else { // Async backup is pending, don't let user delete course. echo $OUTPUT->notification(get_string('pendingasyncerror', 'backup'), 'error'); echo $OUTPUT->container(get_string('pendingasyncdeletedetail', 'backup')); echo $OUTPUT->continue_button($categoryurl); } echo $OUTPUT->footer(); exit; PK$9\1Ƭcompletion.phpnu[. /** * Edit course completion settings * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__.'/../config.php'); require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_unenrol.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_duration.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_grade.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_role.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_course.php'); require_once $CFG->libdir.'/gradelib.php'; require_once($CFG->dirroot.'/course/completion_form.php'); $id = required_param('id', PARAM_INT); // course id // Perform some basic access control checks. if ($id) { if($id == SITEID){ // Don't allow editing of 'site course' using this form. throw new \moodle_exception('cannoteditsiteform'); } if (!$course = $DB->get_record('course', array('id'=>$id))) { throw new \moodle_exception('invalidcourseid'); } require_login($course); $context = context_course::instance($course->id); if (!has_capability('moodle/course:update', $context)) { // User is not allowed to modify course completion. // Check if they can see default completion or edit bulk completion and redirect there. if ($options = core_completion\manager::get_available_completion_options($course->id)) { // Redirect to the first available completion page. redirect(array_key_first($options)); } else { require_capability('moodle/course:update', $context); } } } else { require_login(); throw new \moodle_exception('needcourseid'); } // Set up the page. $PAGE->set_course($course); $PAGE->set_url('/course/completion.php', array('id' => $course->id)); $PAGE->set_title($course->shortname); $PAGE->set_heading($course->fullname); $PAGE->set_pagelayout('admin'); // Create the settings form instance. $form = new course_completion_form('completion.php?id='.$id, array('course' => $course)); if ($form->is_cancelled()){ redirect($CFG->wwwroot.'/course/view.php?id='.$course->id); } else if ($data = $form->get_data()) { $completion = new completion_info($course); // Process criteria unlocking if requested. if (!empty($data->settingsunlock)) { $completion->delete_course_completion_data(); // Return to form (now unlocked). redirect($PAGE->url); } // Delete old criteria. $completion->clear_criteria(false); // Loop through each criteria type and run its update_config() method. global $COMPLETION_CRITERIA_TYPES; foreach ($COMPLETION_CRITERIA_TYPES as $type) { $class = 'completion_criteria_'.$type; $criterion = new $class(); $criterion->update_config($data); } // Handle overall aggregation. $aggdata = array( 'course' => $data->id, 'criteriatype' => null ); $aggregation = new completion_aggregation($aggdata); $aggregation->setMethod($data->overall_aggregation); $aggregation->save(); // Handle activity aggregation. if (empty($data->activity_aggregation)) { $data->activity_aggregation = 0; } $aggdata['criteriatype'] = COMPLETION_CRITERIA_TYPE_ACTIVITY; $aggregation = new completion_aggregation($aggdata); $aggregation->setMethod($data->activity_aggregation); $aggregation->save(); // Handle course aggregation. if (empty($data->course_aggregation)) { $data->course_aggregation = 0; } $aggdata['criteriatype'] = COMPLETION_CRITERIA_TYPE_COURSE; $aggregation = new completion_aggregation($aggdata); $aggregation->setMethod($data->course_aggregation); $aggregation->save(); // Handle role aggregation. if (empty($data->role_aggregation)) { $data->role_aggregation = 0; } $aggdata['criteriatype'] = COMPLETION_CRITERIA_TYPE_ROLE; $aggregation = new completion_aggregation($aggdata); $aggregation->setMethod($data->role_aggregation); $aggregation->save(); // Trigger an event for course module completion changed. $event = \core\event\course_completion_updated::create( array( 'courseid' => $course->id, 'context' => context_course::instance($course->id) ) ); $event->trigger(); // Redirect to the course main page. $url = new moodle_url('/course/view.php', array('id' => $course->id)); redirect($url); } $renderer = $PAGE->get_renderer('core_course', 'bulk_activity_completion'); // Print the form. echo $OUTPUT->header(); $actionbar = new \core_course\output\completion_action_bar($course->id, $PAGE->url); echo $renderer->render_course_completion_action_bar($actionbar); $form->display(); echo $OUTPUT->footer(); PK$9\oO4lib.phpnu[. /** * Library of useful functions * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_course */ defined('MOODLE_INTERNAL') || die; use core\{clock, di}; use core\hook; use core_course\external\course_summary_exporter; use core_courseformat\base as course_format; use core_courseformat\formatactions; use core_courseformat\sectiondelegate; use core\output\local\action_menu\subpanel as action_menu_subpanel; require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->libdir.'/filelib.php'); require_once($CFG->libdir.'/datalib.php'); require_once($CFG->dirroot.'/course/format/lib.php'); define('COURSE_MAX_LOGS_PER_PAGE', 1000); // Records. define('COURSE_MAX_RECENT_PERIOD', 172800); // Two days, in seconds. /** * Number of courses to display when summaries are included. * @var int * @deprecated since 2.4, use $CFG->courseswithsummarieslimit instead. */ define('COURSE_MAX_SUMMARIES_PER_PAGE', 10); // Max courses in log dropdown before switching to optional. define('COURSE_MAX_COURSES_PER_DROPDOWN', 1000); // Max users in log dropdown before switching to optional. define('COURSE_MAX_USERS_PER_DROPDOWN', 1000); define('FRONTPAGENEWS', '0'); define('FRONTPAGECATEGORYNAMES', '2'); define('FRONTPAGECATEGORYCOMBO', '4'); define('FRONTPAGEENROLLEDCOURSELIST', '5'); define('FRONTPAGEALLCOURSELIST', '6'); define('FRONTPAGECOURSESEARCH', '7'); // Important! Replaced with $CFG->frontpagecourselimit - maximum number of courses displayed on the frontpage. define('EXCELROWS', 65535); define('FIRSTUSEDEXCELROW', 3); define('MOD_CLASS_ACTIVITY', 0); define('MOD_CLASS_RESOURCE', 1); define('COURSE_TIMELINE_ALLINCLUDINGHIDDEN', 'allincludinghidden'); define('COURSE_TIMELINE_ALL', 'all'); define('COURSE_TIMELINE_PAST', 'past'); define('COURSE_TIMELINE_INPROGRESS', 'inprogress'); define('COURSE_TIMELINE_FUTURE', 'future'); define('COURSE_TIMELINE_SEARCH', 'search'); define('COURSE_FAVOURITES', 'favourites'); define('COURSE_TIMELINE_HIDDEN', 'hidden'); define('COURSE_CUSTOMFIELD', 'customfield'); define('COURSE_DB_QUERY_LIMIT', 1000); /** Searching for all courses that have no value for the specified custom field. */ define('COURSE_CUSTOMFIELD_EMPTY', -1); // Course activity chooser footer default display option. define('COURSE_CHOOSER_FOOTER_NONE', 'hidden'); // Download course content options. define('DOWNLOAD_COURSE_CONTENT_DISABLED', 0); define('DOWNLOAD_COURSE_CONTENT_ENABLED', 1); define('DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT', 2); function make_log_url($module, $url) { switch ($module) { case 'course': if (strpos($url, 'report/') === 0) { // there is only one report type, course reports are deprecated $url = "/$url"; break; } case 'file': case 'login': case 'lib': case 'admin': case 'category': case 'mnet course': if (strpos($url, '../') === 0) { $url = ltrim($url, '.'); } else { $url = "/course/$url"; } break; case 'calendar': $url = "/calendar/$url"; break; case 'user': case 'blog': $url = "/$module/$url"; break; case 'upload': $url = $url; break; case 'coursetags': $url = '/'.$url; break; case 'library': case '': $url = '/'; break; case 'message': $url = "/message/$url"; break; case 'notes': $url = "/notes/$url"; break; case 'tag': $url = "/tag/$url"; break; case 'role': $url = '/'.$url; break; case 'grade': $url = "/grade/$url"; break; default: $url = "/mod/$module/$url"; break; } //now let's sanitise urls - there might be some ugly nasties:-( $parts = explode('?', $url); $script = array_shift($parts); if (strpos($script, 'http') === 0) { $script = clean_param($script, PARAM_URL); } else { $script = clean_param($script, PARAM_PATH); } $query = ''; if ($parts) { $query = implode('', $parts); $query = str_replace('&', '&', $query); // both & and & are stored in db :-| $parts = explode('&', $query); $eq = urlencode('='); foreach ($parts as $key=>$part) { $part = urlencode(urldecode($part)); $part = str_replace($eq, '=', $part); $parts[$key] = $part; } $query = '?'.implode('&', $parts); } return $script.$query; } function build_mnet_logs_array($hostid, $course, $user=0, $date=0, $order="l.time ASC", $limitfrom='', $limitnum='', $modname="", $modid=0, $modaction="", $groupid=0) { global $CFG, $DB; // It is assumed that $date is the GMT time of midnight for that day, // and so the next 86400 seconds worth of logs are printed. /// Setup for group handling. // TODO: I don't understand group/context/etc. enough to be able to do // something interesting with it here // What is the context of a remote course? /// If the group mode is separate, and this user does not have editing privileges, /// then only the user's group can be viewed. //if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/course:managegroups', context_course::instance($course->id))) { // $groupid = get_current_group($course->id); //} /// If this course doesn't have groups, no groupid can be specified. //else if (!$course->groupmode) { // $groupid = 0; //} $groupid = 0; $joins = array(); $where = ''; $qry = "SELECT l.*, u.firstname, u.lastname, u.picture FROM {mnet_log} l LEFT JOIN {user} u ON l.userid = u.id WHERE "; $params = array(); $where .= "l.hostid = :hostid"; $params['hostid'] = $hostid; // TODO: Is 1 really a magic number referring to the sitename? if ($course != SITEID || $modid != 0) { $where .= " AND l.course=:courseid"; $params['courseid'] = $course; } if ($modname) { $where .= " AND l.module = :modname"; $params['modname'] = $modname; } if ('site_errors' === $modid) { $where .= " AND ( l.action='error' OR l.action='infected' )"; } else if ($modid) { //TODO: This assumes that modids are the same across sites... probably //not true $where .= " AND l.cmid = :modid"; $params['modid'] = $modid; } if ($modaction) { $firstletter = substr($modaction, 0, 1); if ($firstletter == '-') { $where .= " AND ".$DB->sql_like('l.action', ':modaction', false, true, true); $params['modaction'] = '%'.substr($modaction, 1).'%'; } else { $where .= " AND ".$DB->sql_like('l.action', ':modaction', false); $params['modaction'] = '%'.$modaction.'%'; } } if ($user) { $where .= " AND l.userid = :user"; $params['user'] = $user; } if ($date) { $enddate = $date + 86400; $where .= " AND l.time > :date AND l.time < :enddate"; $params['date'] = $date; $params['enddate'] = $enddate; } $result = array(); $result['totalcount'] = $DB->count_records_sql("SELECT COUNT('x') FROM {mnet_log} l WHERE $where", $params); if(!empty($result['totalcount'])) { $where .= " ORDER BY $order"; $result['logs'] = $DB->get_records_sql("$qry $where", $params, $limitfrom, $limitnum); } else { $result['logs'] = array(); } return $result; } /** * Checks the integrity of the course data. * * In summary - compares course_sections.sequence and course_modules.section. * * More detailed, checks that: * - course_sections.sequence contains each module id not more than once in the course * - for each moduleid from course_sections.sequence the field course_modules.section * refers to the same section id (this means course_sections.sequence is more * important if they are different) * - ($fullcheck only) each module in the course is present in one of * course_sections.sequence * - ($fullcheck only) removes non-existing course modules from section sequences * * If there are any mismatches, the changes are made and records are updated in DB. * * Course cache is NOT rebuilt if there are any errors! * * This function is used each time when course cache is being rebuilt with $fullcheck = false * and in CLI script admin/cli/fix_course_sequence.php with $fullcheck = true * * @param int $courseid id of the course * @param array $rawmods result of funciton {@link get_course_mods()} - containst * the list of enabled course modules in the course. Retrieved from DB if not specified. * Argument ignored in cashe of $fullcheck, the list is retrieved form DB anyway. * @param array $sections records from course_sections table for this course. * Retrieved from DB if not specified * @param bool $fullcheck Will add orphaned modules to their sections and remove non-existing * course modules from sequences. Only to be used in site maintenance mode when we are * sure that another user is not in the middle of the process of moving/removing a module. * @param bool $checkonly Only performs the check without updating DB, outputs all errors as debug messages. * @return array|false array of messages with found problems. Empty output means everything is ok */ function course_integrity_check($courseid, $rawmods = null, $sections = null, $fullcheck = false, $checkonly = false) { global $DB; $messages = array(); if ($sections === null) { $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section', 'id,section,sequence'); } if ($fullcheck) { // Retrieve all records from course_modules regardless of module type visibility. $rawmods = $DB->get_records('course_modules', array('course' => $courseid), 'id', 'id,section'); } if ($rawmods === null) { $rawmods = get_course_mods($courseid); } if (!$fullcheck && (empty($sections) || empty($rawmods))) { // If either of the arrays is empty, no modules are displayed anyway. return true; } $debuggingprefix = 'Failed integrity check for course ['.$courseid.']. '; // First make sure that each module id appears in section sequences only once. // If it appears in several section sequences the last section wins. // If it appears twice in one section sequence, the first occurence wins. $modsection = array(); foreach ($sections as $sectionid => $section) { $sections[$sectionid]->newsequence = $section->sequence; if (!empty($section->sequence)) { $sequence = explode(",", $section->sequence); $sequenceunique = array_unique($sequence); if (count($sequenceunique) != count($sequence)) { // Some course module id appears in this section sequence more than once. ksort($sequenceunique); // Preserve initial order of modules. $sequence = array_values($sequenceunique); $sections[$sectionid]->newsequence = join(',', $sequence); $messages[] = $debuggingprefix.'Sequence for course section ['. $sectionid.'] is "'.$sections[$sectionid]->sequence.'", must be "'.$sections[$sectionid]->newsequence.'"'; } foreach ($sequence as $cmid) { if (array_key_exists($cmid, $modsection) && isset($rawmods[$cmid])) { // Some course module id appears to be in more than one section's sequences. $wrongsectionid = $modsection[$cmid]; $sections[$wrongsectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$wrongsectionid]->newsequence. ','), ','); $messages[] = $debuggingprefix.'Course module ['.$cmid.'] must be removed from sequence of section ['. $wrongsectionid.'] because it is also present in sequence of section ['.$sectionid.']'; } $modsection[$cmid] = $sectionid; } } } // Add orphaned modules to their sections if they exist or to section 0 otherwise. if ($fullcheck) { foreach ($rawmods as $cmid => $mod) { if (!isset($modsection[$cmid])) { // This is a module that is not mentioned in course_section.sequence at all. // Add it to the section $mod->section or to the last available section. if ($mod->section && isset($sections[$mod->section])) { $modsection[$cmid] = $mod->section; } else { $firstsection = reset($sections); $modsection[$cmid] = $firstsection->id; } $sections[$modsection[$cmid]]->newsequence = trim($sections[$modsection[$cmid]]->newsequence.','.$cmid, ','); $messages[] = $debuggingprefix.'Course module ['.$cmid.'] is missing from sequence of section ['. $modsection[$cmid].']'; } } foreach ($modsection as $cmid => $sectionid) { if (!isset($rawmods[$cmid])) { // Section $sectionid refers to module id that does not exist. $sections[$sectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$sectionid]->newsequence.','), ','); $messages[] = $debuggingprefix.'Course module ['.$cmid. '] does not exist but is present in the sequence of section ['.$sectionid.']'; } } } // Update changed sections. if (!$checkonly && !empty($messages)) { foreach ($sections as $sectionid => $section) { if ($section->newsequence !== $section->sequence) { $DB->update_record('course_sections', array('id' => $sectionid, 'sequence' => $section->newsequence)); } } } // Now make sure that all modules point to the correct sections. foreach ($rawmods as $cmid => $mod) { if (isset($modsection[$cmid]) && $modsection[$cmid] != $mod->section) { if (!$checkonly) { $DB->update_record('course_modules', array('id' => $cmid, 'section' => $modsection[$cmid])); } $messages[] = $debuggingprefix.'Course module ['.$cmid. '] points to section ['.$mod->section.'] instead of ['.$modsection[$cmid].']'; } } return $messages; } /** * Returns an array where the key is the module name (component name without 'mod_') * and the value is a lang_string object with a human-readable string. * * @param bool $plural If true, the function returns the plural forms of the names. * @param bool $resetcache If true, the static cache will be reset * @return lang_string[] Localised human-readable names of all used modules. */ function get_module_types_names($plural = false, $resetcache = false) { static $modnames = null; global $DB, $CFG; if ($modnames === null || empty($modnames[0]) || $resetcache) { $modnames = array(0 => array(), 1 => array()); if ($allmods = $DB->get_records("modules")) { foreach ($allmods as $mod) { if (file_exists("$CFG->dirroot/mod/$mod->name/lib.php") && $mod->visible) { $modnames[0][$mod->name] = get_string("modulename", "$mod->name", null, true); $modnames[1][$mod->name] = get_string("modulenameplural", "$mod->name", null, true); } } } } return $modnames[(int)$plural]; } /** * Set highlighted section. Only one section can be highlighted at the time. * * @param int $courseid course id * @param int $marker highlight section with this number, 0 means remove higlightin * @return void */ function course_set_marker($courseid, $marker) { global $DB, $COURSE; $DB->set_field("course", "marker", $marker, array('id' => $courseid)); if ($COURSE && $COURSE->id == $courseid) { $COURSE->marker = $marker; } core_courseformat\base::reset_course_cache($courseid); course_modinfo::clear_instance_cache($courseid); } /** * For a given course section, marks it visible or hidden, * and does the same for every activity in that section * * @param int $courseid course id * @param int $sectionnumber The section number to adjust * @param int $visibility The new visibility * @return array A list of resources which were hidden in the section */ function set_section_visible($courseid, $sectionnumber, $visibility) { global $DB; $resourcestotoggle = array(); if ($section = $DB->get_record("course_sections", array("course"=>$courseid, "section"=>$sectionnumber))) { course_update_section($courseid, $section, array('visible' => $visibility)); // Determine which modules are visible for AJAX update $modules = !empty($section->sequence) ? explode(',', $section->sequence) : array(); if (!empty($modules)) { list($insql, $params) = $DB->get_in_or_equal($modules); $select = 'id ' . $insql . ' AND visible = ?'; array_push($params, $visibility); if (!$visibility) { $select .= ' AND visibleold = 1'; } $resourcestotoggle = $DB->get_fieldset_select('course_modules', 'id', $select, $params); } } return $resourcestotoggle; } /** * Return the course category context for the category with id $categoryid, except * that if $categoryid is 0, return the system context. * * @param integer $categoryid a category id or 0. * @return context the corresponding context */ function get_category_or_system_context($categoryid) { if ($categoryid) { return context_coursecat::instance($categoryid, IGNORE_MISSING); } else { return context_system::instance(); } } /** * Does the user have permission to edit things in this category? * * @param integer $categoryid The id of the category we are showing, or 0 for system context. * @return boolean has_any_capability(array(...), ...); in the appropriate context. */ function can_edit_in_category($categoryid = 0) { $context = get_category_or_system_context($categoryid); return has_any_capability(array('moodle/category:manage', 'moodle/course:create'), $context); } /// MODULE FUNCTIONS ///////////////////////////////////////////////////////////////// function add_course_module($mod) { global $DB; $mod->added = time(); unset($mod->id); $cmid = $DB->insert_record("course_modules", $mod); rebuild_course_cache($mod->course, true); return $cmid; } /** * Creates a course section and adds it to the specified position * * @param int|stdClass $courseorid course id or course object * @param int $position position to add to, 0 means to the end. If position is greater than * number of existing secitons, the section is added to the end. This will become sectionnum of the * new section. All existing sections at this or bigger position will be shifted down. * @param bool $skipcheck the check has already been made and we know that the section with this position does not exist * @return stdClass created section object */ function course_create_section($courseorid, $position = 0, $skipcheck = false) { return formatactions::section($courseorid)->create($position, $skipcheck); } /** * Creates missing course section(s) and rebuilds course cache * * @param int|stdClass $courseorid course id or course object * @param int|array $sections list of relative section numbers to create * @return bool if there were any sections created */ function course_create_sections_if_missing($courseorid, $sections) { if (!is_array($sections)) { $sections = array($sections); } return formatactions::section($courseorid)->create_if_missing($sections); } /** * Adds an existing module to the section * * Updates both tables {course_sections} and {course_modules} * * Note: This function does not use modinfo PROVIDED that the section you are * adding the module to already exists. If the section does not exist, it will * build modinfo if necessary and create the section. * * @param int|stdClass $courseorid course id or course object * @param int $cmid id of the module already existing in course_modules table * @param int $sectionnum relative number of the section (field course_sections.section) * If section does not exist it will be created * @param int|stdClass $beforemod id or object with field id corresponding to the module * before which the module needs to be included. Null for inserting in the * end of the section * @param string $modname Optional, name of the module in the modules table. We need to do some checks * to see if this module type can be displayed to the course page. * If not passed a DB query will need to be run instead. * @return int The course_sections ID where the module is inserted * @throws moodle_exception if a module that has feature flag FEATURE_CAN_DISPLAY set to false is attempted to be moved to * a section number other than 0. */ function course_add_cm_to_section($courseorid, $cmid, $sectionnum, $beforemod = null, string $modname = '') { global $DB, $COURSE; if (is_object($beforemod)) { $beforemod = $beforemod->id; } if (is_object($courseorid)) { $courseid = $courseorid->id; } else { $courseid = $courseorid; } if (!$modname) { $sql = "SELECT name FROM {modules} m JOIN {course_modules} cm ON cm.module = m.id WHERE cm.id = :cmid"; $modname = $DB->get_field_sql($sql, ['cmid' => $cmid], MUST_EXIST); } // Modules not visible on the course must ALWAYS be in section 0. if ($sectionnum != 0 && !course_modinfo::is_mod_type_visible_on_course($modname)) { throw new moodle_exception("Modules with FEATURE_CAN_DISPLAY set to false can not be moved from section 0"); } // Do not try to use modinfo here, there is no guarantee it is valid! $section = $DB->get_record('course_sections', array('course' => $courseid, 'section' => $sectionnum), '*', IGNORE_MISSING); if (!$section) { // This function call requires modinfo. course_create_sections_if_missing($courseorid, $sectionnum); $section = $DB->get_record('course_sections', array('course' => $courseid, 'section' => $sectionnum), '*', MUST_EXIST); } $modarray = explode(",", trim($section->sequence)); if (empty($section->sequence)) { $newsequence = "$cmid"; } else if ($beforemod && ($key = moodle_array_keys_filter($modarray, $beforemod))) { $insertarray = array($cmid, $beforemod); array_splice($modarray, $key[0], 1, $insertarray); $newsequence = implode(",", $modarray); } else { $newsequence = "$section->sequence,$cmid"; } $DB->set_field("course_sections", "sequence", $newsequence, array("id" => $section->id)); $DB->set_field('course_modules', 'section', $section->id, array('id' => $cmid)); rebuild_course_cache($courseid, true); return $section->id; // Return course_sections ID that was used. } /** * Change the group mode of a course module. * * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}. * * @param int $id course module ID. * @param int $groupmode the new groupmode value. * @return bool True if the $groupmode was updated. */ function set_coursemodule_groupmode($id, $groupmode) { global $DB; $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,groupmode', MUST_EXIST); if ($cm->groupmode != $groupmode) { $DB->set_field('course_modules', 'groupmode', $groupmode, array('id' => $cm->id)); \course_modinfo::purge_course_module_cache($cm->course, $cm->id); rebuild_course_cache($cm->course, false, true); } return ($cm->groupmode != $groupmode); } function set_coursemodule_idnumber($id, $idnumber) { global $DB; $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,idnumber', MUST_EXIST); if ($cm->idnumber != $idnumber) { $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id)); \course_modinfo::purge_course_module_cache($cm->course, $cm->id); rebuild_course_cache($cm->course, false, true); } return ($cm->idnumber != $idnumber); } /** * Set downloadcontent value to course module. * * @param int $id The id of the module. * @param bool $downloadcontent Whether the module can be downloaded when download course content is enabled. * @return bool True if downloadcontent has been updated, false otherwise. */ function set_downloadcontent(int $id, bool $downloadcontent): bool { global $DB; $cm = $DB->get_record('course_modules', ['id' => $id], 'id, course, downloadcontent', MUST_EXIST); if ($cm->downloadcontent != $downloadcontent) { $DB->set_field('course_modules', 'downloadcontent', $downloadcontent, ['id' => $cm->id]); rebuild_course_cache($cm->course, true); } return ($cm->downloadcontent != $downloadcontent); } /** * Set the visibility of a module and inherent properties. * * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}. * * From 2.4 the parameter $prevstateoverrides has been removed, the logic it triggered * has been moved to {@link set_section_visible()} which was the only place from which * the parameter was used. * * If $rebuildcache is set to false, the calling code is responsible for ensuring the cache is purged * and rebuilt as appropriate. Consider using this if set_coursemodule_visible is called multiple times * (e.g. in a loop). * * @param int $cmid course module id * @param int $visible state of the module * @param int $visibleoncoursepage state of the module on the course page * @param bool $rebuildcache If true (default), perform a partial cache purge and rebuild. * @return bool false when the module was not found, true otherwise */ function set_coursemodule_visible($cmid, $visible, $visibleoncoursepage = 1, bool $rebuildcache = true) { $coursecontext = context_module::instance($cmid)->get_course_context(); return formatactions::cm($coursecontext->instanceid)->set_visibility($cmid, $visible, $visibleoncoursepage, $rebuildcache); } /** * Changes the course module name * * @param int $cmid course module id * @param string $name new value for a name * @return bool whether a change was made */ function set_coursemodule_name($cmid, $name) { $coursecontext = context_module::instance($cmid)->get_course_context(); return formatactions::cm($coursecontext->instanceid)->rename($cmid, $name); } /** * This function will handle the whole deletion process of a module. This includes calling * the modules delete_instance function, deleting files, events, grades, conditional data, * the data in the course_module and course_sections table and adding a module deletion * event to the DB. * * @param int $cmid the course module id * @param bool $async whether or not to try to delete the module using an adhoc task. Async also depends on a plugin hook. * @throws moodle_exception * @since Moodle 2.5 */ function course_delete_module($cmid, $async = false) { // Check the 'course_module_background_deletion_recommended' hook first. // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested. // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it. // It's up to plugins to handle things like whether or not they are enabled. if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) { foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { if ($pluginfunction()) { return course_module_flag_for_async_deletion($cmid); } } } } global $CFG, $DB; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->libdir.'/questionlib.php'); require_once($CFG->dirroot.'/blog/lib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); // Get the course module. if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) { return true; } // Get the module context. $modcontext = context_module::instance($cm->id); // Get the course module name. $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST); // Get the file location of the delete_instance function for this module. $modlib = "$CFG->dirroot/mod/$modulename/lib.php"; // Include the file required to call the delete_instance function for this module. if (file_exists($modlib)) { require_once($modlib); } else { throw new moodle_exception('cannotdeletemodulemissinglib', '', '', null, "Cannot delete this module as the file mod/$modulename/lib.php is missing."); } // Warning! there is very similar code in remove_course_contents. // If you are changing this code, you probably need to change that too. $deleteinstancefunction = $modulename . '_delete_instance'; // Ensure the delete_instance function exists for this module. if (!function_exists($deleteinstancefunction)) { throw new moodle_exception('cannotdeletemodulemissingfunc', '', '', null, "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php."); } // Allow plugins to use this course module before we completely delete it. if ($pluginsfunction = get_plugins_with_function('pre_course_module_delete')) { foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { $pluginfunction($cm); } } } if (empty($cm->instance)) { throw new moodle_exception('cannotdeletemodulemissinginstance', '', '', null, "Cannot delete course module with ID $cm->id because it does not have a valid activity instance."); } // Call the delete_instance function, if it returns false throw an exception. if (!$deleteinstancefunction($cm->instance)) { throw new moodle_exception('cannotdeletemoduleinstance', '', '', null, "Cannot delete the module $modulename (instance)."); } // We delete the questions after the activity database is removed, // because questions are referenced via question reference tables // and cannot be deleted while the activities that use them still exist. question_delete_activity($cm); // Remove all module files in case modules forget to do that. $fs = get_file_storage(); $fs->delete_area_files($modcontext->id); // Delete events from calendar. if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename))) { $coursecontext = context_course::instance($cm->course); foreach($events as $event) { $event->context = $coursecontext; $calendarevent = calendar_event::load($event); $calendarevent->delete(); } } // Delete grade items, outcome items and grades attached to modules. if ($grade_items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $modulename, 'iteminstance' => $cm->instance, 'courseid' => $cm->course))) { foreach ($grade_items as $grade_item) { $grade_item->delete('moddelete'); } } // Delete associated blogs and blog tag instances. blog_remove_associations_for_module($modcontext->id); // Delete completion and availability data; it is better to do this even if the // features are not turned on, in case they were turned on previously (these will be // very quick on an empty table). $DB->delete_records('course_modules_completion', array('coursemoduleid' => $cm->id)); $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]); $DB->delete_records('course_completion_criteria', array('moduleinstance' => $cm->id, 'course' => $cm->course, 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY)); // Delete all tag instances associated with the instance of this module. core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id); core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id); // Notify the competency subsystem. \core_competency\api::hook_course_module_deleted($cm); // Delete the context. context_helper::delete_instance(CONTEXT_MODULE, $cm->id); // Delete the module from the course_modules table. $DB->delete_records('course_modules', array('id' => $cm->id)); // Delete module from that section. if (!delete_mod_from_section($cm->id, $cm->section)) { throw new moodle_exception('cannotdeletemodulefromsection', '', '', null, "Cannot delete the module $modulename (instance) from section."); } // Trigger event for course module delete action. $event = \core\event\course_module_deleted::create(array( 'courseid' => $cm->course, 'context' => $modcontext, 'objectid' => $cm->id, 'other' => array( 'modulename' => $modulename, 'instanceid' => $cm->instance, ) )); $event->add_record_snapshot('course_modules', $cm); $event->trigger(); \course_modinfo::purge_course_module_cache($cm->course, $cm->id); rebuild_course_cache($cm->course, false, true); } /** * Schedule a course module for deletion in the background using an adhoc task. * * This method should not be called directly. Instead, please use course_delete_module($cmid, true), to denote async deletion. * The real deletion of the module is handled by the task, which calls 'course_delete_module($cmid)'. * * @param int $cmid the course module id. * @return ?bool whether the module was successfully scheduled for deletion. * @throws \moodle_exception */ function course_module_flag_for_async_deletion($cmid) { global $CFG, $DB, $USER; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->libdir.'/questionlib.php'); require_once($CFG->dirroot.'/blog/lib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); // Get the course module. if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) { return true; } // We need to be reasonably certain the deletion is going to succeed before we background the process. // Make the necessary delete_instance checks, etc. before proceeding further. Throw exceptions if required. // Get the course module name. $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST); // Get the file location of the delete_instance function for this module. $modlib = "$CFG->dirroot/mod/$modulename/lib.php"; // Include the file required to call the delete_instance function for this module. if (file_exists($modlib)) { require_once($modlib); } else { throw new \moodle_exception('cannotdeletemodulemissinglib', '', '', null, "Cannot delete this module as the file mod/$modulename/lib.php is missing."); } $deleteinstancefunction = $modulename . '_delete_instance'; // Ensure the delete_instance function exists for this module. if (!function_exists($deleteinstancefunction)) { throw new \moodle_exception('cannotdeletemodulemissingfunc', '', '', null, "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php."); } // We are going to defer the deletion as we can't be sure how long the module's pre_delete code will run for. $cm->deletioninprogress = '1'; $DB->update_record('course_modules', $cm); // Create an adhoc task for the deletion of the course module. The task takes an array of course modules for removal. $removaltask = new \core_course\task\course_delete_modules(); $removaltask->set_custom_data(array( 'cms' => array($cm), 'userid' => $USER->id, 'realuserid' => \core\session\manager::get_realuser()->id )); // Queue the task for the next run. \core\task\manager::queue_adhoc_task($removaltask); // Reset the course cache to hide the module. rebuild_course_cache($cm->course, true); } /** * Checks whether the given course has any course modules scheduled for adhoc deletion. * * @param int $courseid the id of the course. * @param bool $onlygradable whether to check only gradable modules or all modules. * @return bool true if the course contains any modules pending deletion, false otherwise. */ function course_modules_pending_deletion(int $courseid, bool $onlygradable = false): bool { if (empty($courseid)) { return false; } if ($onlygradable) { // Fetch modules with grade items. if (!$coursegradeitems = grade_item::fetch_all(['itemtype' => 'mod', 'courseid' => $courseid])) { // Return early when there is none. return false; } } $modinfo = get_fast_modinfo($courseid); foreach ($modinfo->get_cms() as $module) { if ($module->deletioninprogress == '1') { if ($onlygradable) { // Check if the module being deleted is in the list of course modules with grade items. foreach ($coursegradeitems as $coursegradeitem) { if ($coursegradeitem->itemmodule == $module->modname && $coursegradeitem->iteminstance == $module->instance) { // The module being deleted is within the gradable modules. return true; } } } else { return true; } } } return false; } /** * Checks whether the course module, as defined by modulename and instanceid, is scheduled for deletion within the given course. * * @param int $courseid the course id. * @param string $modulename the module name. E.g. 'assign', 'book', etc. * @param int $instanceid the module instance id. * @return bool true if the course module is pending deletion, false otherwise. */ function course_module_instance_pending_deletion($courseid, $modulename, $instanceid) { if (empty($courseid) || empty($modulename) || empty($instanceid)) { return false; } $modinfo = get_fast_modinfo($courseid); $instances = $modinfo->get_instances_of($modulename); return isset($instances[$instanceid]) && $instances[$instanceid]->deletioninprogress; } function delete_mod_from_section($modid, $sectionid) { global $DB; if ($section = $DB->get_record("course_sections", array("id"=>$sectionid)) ) { $modarray = explode(",", $section->sequence); if ($key = moodle_array_keys_filter($modarray, $modid)) { array_splice($modarray, $key[0], 1); $newsequence = implode(",", $modarray); $DB->set_field("course_sections", "sequence", $newsequence, array("id"=>$section->id)); rebuild_course_cache($section->course, true); return true; } else { return false; } } return false; } /** * This function updates the calendar events from the information stored in the module table and the course * module table. * * @param string $modulename Module name * @param stdClass $instance Module object. Either the $instance or the $cm must be supplied. * @param stdClass $cm Course module object. Either the $instance or the $cm must be supplied. * @return bool Returns true if calendar events are updated. * @since Moodle 3.3.4 */ function course_module_update_calendar_events($modulename, $instance = null, $cm = null) { global $DB; if (isset($instance) || isset($cm)) { if (!isset($instance)) { $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST); } if (!isset($cm)) { $cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course); } if (!empty($cm)) { course_module_calendar_event_update_process($instance, $cm); } return true; } return false; } /** * Update all instances through out the site or in a course. * * @param string $modulename Module type to update. * @param integer $courseid Course id to update events. 0 for the whole site. * @return bool Returns True if the update was successful. * @since Moodle 3.3.4 */ function course_module_bulk_update_calendar_events($modulename, $courseid = 0) { global $DB; $instances = null; if ($courseid) { if (!$instances = $DB->get_records($modulename, array('course' => $courseid))) { return false; } } else { if (!$instances = $DB->get_records($modulename)) { return false; } } foreach ($instances as $instance) { if ($cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course)) { course_module_calendar_event_update_process($instance, $cm); } } return true; } /** * Calendar events for a module instance are updated. * * @param stdClass $instance Module instance object. * @param stdClass $cm Course Module object. * @since Moodle 3.3.4 */ function course_module_calendar_event_update_process($instance, $cm) { global $CFG; // We need to call *_refresh_events() first because some modules delete 'old' events at the end of the code which // will remove the completion events. include_once("$CFG->dirroot/mod/$cm->modname/lib.php"); $refresheventsfunction = $cm->modname . '_refresh_events'; if (function_exists($refresheventsfunction)) { call_user_func($refresheventsfunction, $cm->course, $instance, $cm); } $completionexpected = (!empty($cm->completionexpected)) ? $cm->completionexpected : null; \core_completion\api::update_completion_date_event($cm->id, $cm->modname, $instance, $completionexpected); } /** * Moves a section within a course, from a position to another. * Be very careful: $section and $destination refer to section number, * not id!. * * @param object $course * @param int $section Section number (not id!!!) * @param int $destination * @param bool $ignorenumsections * @return boolean Result */ function move_section_to($course, $section, $destination, $ignorenumsections = false) { /// Moves a whole course section up and down within the course global $USER, $DB; if (!$destination && $destination != 0) { return true; } // compartibility with course formats using field 'numsections' $courseformatoptions = course_get_format($course)->get_format_options(); if ((!$ignorenumsections && array_key_exists('numsections', $courseformatoptions) && ($destination > $courseformatoptions['numsections'])) || ($destination < 1)) { return false; } // Get all sections for this course and re-order them (2 of them should now share the same section number) if (!$sections = $DB->get_records_menu('course_sections', array('course' => $course->id), 'section ASC, id ASC', 'id, section')) { return false; } $movedsections = reorder_sections($sections, $section, $destination); // Update all sections. Do this in 2 steps to avoid breaking database // uniqueness constraint $transaction = $DB->start_delegated_transaction(); foreach ($movedsections as $id => $position) { if ((int) $sections[$id] !== $position) { $DB->set_field('course_sections', 'section', -$position, ['id' => $id]); // Invalidate the section cache by given section id. course_modinfo::purge_course_section_cache_by_id($course->id, $id); } } foreach ($movedsections as $id => $position) { if ((int) $sections[$id] !== $position) { $DB->set_field('course_sections', 'section', $position, ['id' => $id]); // Invalidate the section cache by given section id. course_modinfo::purge_course_section_cache_by_id($course->id, $id); } } // If we move the highlighted section itself, then just highlight the destination. // Adjust the higlighted section location if we move something over it either direction. if ($section == $course->marker) { course_set_marker($course->id, $destination); } else if ($section > $course->marker && $course->marker >= $destination) { course_set_marker($course->id, $course->marker+1); } else if ($section < $course->marker && $course->marker <= $destination) { course_set_marker($course->id, $course->marker-1); } $transaction->allow_commit(); rebuild_course_cache($course->id, true, true); return true; } /** * This method will delete a course section and may delete all modules inside it. * * No permissions are checked here, use {@link course_can_delete_section()} to * check if section can actually be deleted. * * @param int|stdClass $course * @param int|stdClass|section_info $sectionornum * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it. * @param bool $async whether or not to try to delete the section using an adhoc task. Async also depends on a plugin hook. * @return bool whether section was deleted */ function course_delete_section($course, $sectionornum, $forcedeleteifnotempty = true, $async = false) { $sectionnum = (is_object($sectionornum)) ? $sectionornum->section : (int)$sectionornum; $sectioninfo = get_fast_modinfo($course)->get_section_info($sectionnum); if (!$sectioninfo) { return false; } return formatactions::section($course)->delete($sectioninfo, $forcedeleteifnotempty, $async); } /** * Course section deletion, using an adhoc task for deletion of the modules it contains. * 1. Schedule all modules within the section for adhoc removal. * 2. Move all modules to course section 0. * 3. Delete the resulting empty section. * * @param \stdClass $section the section to schedule for deletion. * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules. * @return bool true if the section was scheduled for deletion, false otherwise. */ function course_delete_section_async($section, $forcedeleteifnotempty = true) { if (!is_object($section) || empty($section->id) || empty($section->course)) { return false; } $sectioninfo = get_fast_modinfo($section->course)->get_section_info_by_id($section->id); if (!$sectioninfo) { return false; } return formatactions::section($section->course)->delete_async($sectioninfo, $forcedeleteifnotempty); } /** * Updates the course section * * This function does not check permissions or clean values - this has to be done prior to calling it. * * @param int|stdClass $courseorid * @param stdClass|section_info $section record from course_sections table - it will be updated with the new values * @param array|stdClass $data */ function course_update_section($courseorid, $section, $data) { $sectioninfo = get_fast_modinfo($courseorid)->get_section_info_by_id($section->id); formatactions::section($courseorid)->update($sectioninfo, $data); // Update $section object fields (for legacy compatibility). $data = array_diff_key((array) $data, array_flip(['id', 'course', 'section', 'sequence'])); foreach ($data as $key => $value) { if (property_exists($section, $key)) { $section->$key = $value; } } } /** * Checks if the current user can delete a section (if course format allows it and user has proper permissions). * * @param int|stdClass $course * @param int|stdClass|section_info $section * @return bool */ function course_can_delete_section($course, $section) { if (is_object($section)) { $section = $section->section; } if (!$section) { // Not possible to delete 0-section. return false; } // Course format should allow to delete sections. if (!course_get_format($course)->can_delete_section($section)) { return false; } // Make sure user has capability to update course and move sections. $context = context_course::instance(is_object($course) ? $course->id : $course); if (!has_all_capabilities(array('moodle/course:movesections', 'moodle/course:update'), $context)) { return false; } // Make sure user has capability to delete each activity in this section. $modinfo = get_fast_modinfo($course); if (!empty($modinfo->sections[$section])) { foreach ($modinfo->sections[$section] as $cmid) { if (!has_capability('moodle/course:manageactivities', context_module::instance($cmid))) { return false; } } } return true; } /** * Reordering algorithm for course sections. Given an array of section->section indexed by section->id, * an original position number and a target position number, rebuilds the array so that the * move is made without any duplication of section positions. * Note: The target_position is the position AFTER WHICH the moved section will be inserted. If you want to * insert a section before the first one, you must give 0 as the target (section 0 can never be moved). * * @param array $sections * @param int $origin_position * @param int $target_position * @return array|false */ function reorder_sections($sections, $origin_position, $target_position) { if (!is_array($sections)) { return false; } // We can't move section position 0 if ($origin_position < 1) { echo "We can't move section position 0"; return false; } // Locate origin section in sections array if (!$origin_key = array_search($origin_position, $sections)) { echo "searched position not in sections array"; return false; // searched position not in sections array } // Extract origin section $origin_section = $sections[$origin_key]; unset($sections[$origin_key]); // Find offset of target position (stupid PHP's array_splice requires offset instead of key index!) $found = false; $append_array = array(); foreach ($sections as $id => $position) { if ($found) { $append_array[$id] = $position; unset($sections[$id]); } if ($position == $target_position) { if ($target_position < $origin_position) { $append_array[$id] = $position; unset($sections[$id]); } $found = true; } } // Append moved section $sections[$origin_key] = $origin_section; // Append rest of array (if applicable) if (!empty($append_array)) { foreach ($append_array as $id => $position) { $sections[$id] = $position; } } // Renumber positions $position = 0; foreach ($sections as $id => $p) { $sections[$id] = $position; $position++; } return $sections; } /** * Move the module object $mod to the specified $section * If $beforemod exists then that is the module * before which $modid should be inserted * * @param stdClass|cm_info $mod * @param stdClass|section_info $section * @param int|stdClass $beforemod id or object with field id corresponding to the module * before which the module needs to be included. Null for inserting in the * end of the section * @return int new value for module visibility (0 or 1) */ function moveto_module($mod, $section, $beforemod=NULL) { global $OUTPUT, $DB; if ($section->section != 0 && !course_modinfo::is_mod_type_visible_on_course($mod->modname)) { throw new coding_exception("Modules with FEATURE_CAN_DISPLAY set to false can not be moved from section 0"); } // Current module visibility state - return value of this function. $modvisible = $mod->visible; // Remove original module from original section. if (! delete_mod_from_section($mod->id, $mod->section)) { echo $OUTPUT->notification("Could not delete module from existing section"); } // Add the module into the new section. course_add_cm_to_section($section->course, $mod->id, $section->section, $beforemod, $mod->modname); // If moving to a hidden section then hide module. if ($mod->section != $section->id) { if (!$section->visible && $mod->visible) { // Module was visible but must become hidden after moving to hidden section. $modvisible = 0; set_coursemodule_visible($mod->id, 0); // Set visibleold to 1 so module will be visible when section is made visible. $DB->set_field('course_modules', 'visibleold', 1, array('id' => $mod->id)); } if ($section->visible && !$mod->visible) { // Hidden module was moved to the visible section, restore the module visibility from visibleold. set_coursemodule_visible($mod->id, $mod->visibleold); $modvisible = $mod->visibleold; } } return $modvisible; } /** * Returns the list of all editing actions that current user can perform on the module * * @deprecated since Moodle 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * * @param cm_info $mod The module to produce editing buttons for * @param int $indent The current indenting (default -1 means no move left-right actions) * @param int $sr The section to link back to (used for creating the links) * @return array array of action_link or pix_icon objects */ #[\core\attribute\deprecated( replacement: 'core_courseformat\output\local\content\cm\controlmenu', since: '5.0', mdl: 'MDL-83527', reason: 'Replaced by an output class equivalent.', )] function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { global $COURSE, $SITE, $CFG; \core\deprecation::emit_deprecation(__FUNCTION__); static $str; $coursecontext = context_course::instance($mod->course); $modcontext = context_module::instance($mod->id); $courseformat = course_get_format($mod->get_course()); $usecomponents = $courseformat->supports_components(); $sectioninfo = $mod->get_section_info(); $hasdelegatesection = sectiondelegate::has_delegate_class('mod_'.$mod->modname); $editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign'); $dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport'); // No permission to edit anything. if (!has_any_capability($editcaps, $modcontext) and !has_all_capabilities($dupecaps, $coursecontext)) { return array(); } $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext); if (!isset($str)) { $str = get_strings( [ 'delete', 'move', 'moveright', 'moveleft', 'editsettings', 'duplicate', 'availability', ], 'moodle' ); $str->assign = get_string('assignroles', 'role'); $str->groupmode = get_string('groupmode', 'group'); } $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey())); if ($sr !== null) { $baseurl->param('sr', $sr); } $actions = array(); // Update. if ($hasmanageactivities) { $actions['update'] = new action_menu_link_secondary( new moodle_url($baseurl, array('update' => $mod->id)), new pix_icon('i/settings', '', 'moodle', ['class' => 'iconsmall']), $str->editsettings, array('class' => 'editing_update', 'data-action' => 'update') ); } // Move (only for component compatible formats). if ($hasmanageactivities && $usecomponents) { $actions['move'] = new action_menu_link_secondary( new moodle_url($baseurl, [ 'sesskey' => sesskey(), 'copy' => $mod->id, ]), new pix_icon('i/dragdrop', '', 'moodle', ['class' => 'iconsmall']), $str->move, [ 'class' => 'editing_movecm', 'data-action' => 'moveCm', 'data-id' => $mod->id, ] ); } // Indent. if ($hasmanageactivities && $indent >= 0 && !$hasdelegatesection) { $indentlimits = new stdClass(); $indentlimits->min = 0; // Legacy indentation could continue using a limit of 16, // but components based formats will be forced to use one level indentation only. $indentlimits->max = ($usecomponents) ? 1 : 16; if (right_to_left()) { // Exchange arrows on RTL $rightarrow = 't/left'; $leftarrow = 't/right'; } else { $rightarrow = 't/right'; $leftarrow = 't/left'; } if ($indent >= $indentlimits->max) { $enabledclass = 'hidden'; } else { $enabledclass = ''; } $actions['moveright'] = new action_menu_link_secondary( new moodle_url($baseurl, ['id' => $mod->id, 'indent' => '1']), new pix_icon($rightarrow, '', 'moodle', ['class' => 'iconsmall']), $str->moveright, [ 'class' => 'editing_moveright ' . $enabledclass, 'data-action' => ($usecomponents) ? 'cmMoveRight' : 'moveright', 'data-keepopen' => true, 'data-sectionreturn' => $sr, 'data-id' => $mod->id, ] ); if ($indent <= $indentlimits->min) { $enabledclass = 'hidden'; } else { $enabledclass = ''; } $actions['moveleft'] = new action_menu_link_secondary( new moodle_url($baseurl, ['id' => $mod->id, 'indent' => '-1']), new pix_icon($leftarrow, '', 'moodle', ['class' => 'iconsmall']), $str->moveleft, [ 'class' => 'editing_moveleft ' . $enabledclass, 'data-action' => ($usecomponents) ? 'cmMoveLeft' : 'moveleft', 'data-keepopen' => true, 'data-sectionreturn' => $sr, 'data-id' => $mod->id, ] ); } // Hide/Show/Available/Unavailable. if (has_capability('moodle/course:activityvisibility', $modcontext)) { $availabilityclass = $courseformat->get_output_classname('content\\cm\\visibility'); /** @var core_courseformat\output\local\content\cm\visibility */ $availability = new $availabilityclass($courseformat, $sectioninfo, $mod); $availabilityitem = $availability->get_menu_item(); if ($availabilityitem) { $actions['availability'] = $availabilityitem; } } // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php) if (has_all_capabilities($dupecaps, $coursecontext) && plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2) && course_allowed_module($mod->get_course(), $mod->modname)) { $actions['duplicate'] = new action_menu_link_secondary( new moodle_url($baseurl, ['duplicate' => $mod->id]), new pix_icon('t/copy', '', 'moodle', array('class' => 'iconsmall')), $str->duplicate, [ 'class' => 'editing_duplicate', 'data-action' => ($courseformat->supports_components()) ? 'cmDuplicate' : 'duplicate', 'data-sectionreturn' => $sr, 'data-id' => $mod->id, ] ); } // Assign. if (has_capability('moodle/role:assign', $modcontext) && !$hasdelegatesection) { $actions['assign'] = new action_menu_link_secondary( new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)), new pix_icon('t/assignroles', '', 'moodle', array('class' => 'iconsmall')), $str->assign, array('class' => 'editing_assign', 'data-action' => 'assignroles', 'data-sectionreturn' => $sr) ); } // Groupmode. if ($courseformat->show_groupmode($mod) && $usecomponents && !$mod->coursegroupmodeforce) { $groupmodeclass = $courseformat->get_output_classname('content\\cm\\groupmode'); /** @var core_courseformat\output\local\content\cm\groupmode */ $groupmode = new $groupmodeclass($courseformat, $sectioninfo, $mod); $actions['groupmode'] = new action_menu_subpanel( $str->groupmode, $groupmode->get_choice_list(), ['class' => 'editing_groupmode'], new pix_icon('t/groupv', '', 'moodle', ['class' => 'iconsmall']) ); } // Delete. if ($hasmanageactivities) { $actions['delete'] = new action_menu_link_secondary( new moodle_url($baseurl, ['delete' => $mod->id]), new pix_icon('t/delete', '', 'moodle', ['class' => 'iconsmall']), $str->delete, [ 'class' => 'editing_delete text-danger', 'data-action' => ($usecomponents) ? 'cmDelete' : 'delete', 'data-sectionreturn' => $sr, 'data-id' => $mod->id, ] ); } return $actions; } /** * Returns the move action. * * @param cm_info $mod The module to produce a move button for * @param int $sr The section to link back to (used for creating the links) * @return string The markup for the move action, or an empty string if not available. */ function course_get_cm_move(cm_info $mod, $sr = null) { global $OUTPUT; static $str; static $baseurl; $modcontext = context_module::instance($mod->id); $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext); if (!isset($str)) { $str = get_strings(array('move')); } if (!isset($baseurl)) { $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey())); if ($sr !== null) { $baseurl->param('sr', $sr); } } if ($hasmanageactivities) { $pixicon = 'i/dragdrop'; if (!course_ajax_enabled($mod->get_course())) { // Override for course frontpage until we get drag/drop working there. $pixicon = 't/move'; } $attributes = [ 'class' => 'editing_move', 'data-action' => 'move', 'data-sectionreturn' => $sr, 'title' => $str->move, 'aria-label' => $str->move, ]; return html_writer::link( new moodle_url($baseurl, ['copy' => $mod->id]), $OUTPUT->pix_icon($pixicon, '', 'moodle', ['class' => 'iconsmall']), $attributes ); } return ''; } /** * given a course object with shortname & fullname, this function will * truncate the the number of chars allowed and add ... if it was too long */ function course_format_name ($course,$max=100) { $context = context_course::instance($course->id); $shortname = format_string($course->shortname, true, array('context' => $context)); $fullname = format_string($course->fullname, true, array('context' => context_course::instance($course->id))); $str = $shortname.': '. $fullname; if (core_text::strlen($str) <= $max) { return $str; } else { return core_text::substr($str,0,$max-3).'...'; } } /** * Is the user allowed to add this type of module to this course? * @param object $course the course settings. Only $course->id is used. * @param string $modname the module name. E.g. 'forum' or 'quiz'. * @param \stdClass $user the user to check, defaults to the global user if not provided. * @return bool whether the current user is allowed to add this type of module to this course. */ function course_allowed_module($course, $modname, ?\stdClass $user = null) { global $USER; $user = $user ?? $USER; if (is_numeric($modname)) { throw new coding_exception('Function course_allowed_module no longer supports numeric module ids. Please update your code to pass the module name.'); } $capability = 'mod/' . $modname . ':addinstance'; if (!get_capability_info($capability)) { // Debug warning that the capability does not exist, but no more than once per page. static $warned = array(); $archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); if (!isset($warned[$modname]) && $archetype !== MOD_ARCHETYPE_SYSTEM) { debugging('The module ' . $modname . ' does not define the standard capability ' . $capability , DEBUG_DEVELOPER); $warned[$modname] = 1; } // If the capability does not exist, the module can always be added. return true; } $coursecontext = context_course::instance($course->id); return has_capability($capability, $coursecontext, $user); } /** * Efficiently moves many courses around while maintaining * sortorder in order. * * @param array $courseids is an array of course ids * @param int $categoryid * @return bool success */ function move_courses($courseids, $categoryid) { global $DB; if (empty($courseids)) { // Nothing to do. return false; } if (!$category = $DB->get_record('course_categories', array('id' => $categoryid))) { return false; } $courseids = array_reverse($courseids); $newparent = context_coursecat::instance($category->id); $i = 1; list($where, $params) = $DB->get_in_or_equal($courseids); $dbcourses = $DB->get_records_select('course', 'id ' . $where, $params, '', 'id, category, shortname, fullname'); foreach ($dbcourses as $dbcourse) { $course = new stdClass(); $course->id = $dbcourse->id; $course->timemodified = time(); $course->category = $category->id; $course->sortorder = $category->sortorder + get_max_courses_in_category() - $i++; if ($category->visible == 0) { // Hide the course when moving into hidden category, do not update the visibleold flag - we want to get // to previous state if somebody unhides the category. $course->visible = 0; } $DB->update_record('course', $course); // Update context, so it can be passed to event. $context = context_course::instance($course->id); $context->update_moved($newparent); // Trigger a course updated event. $event = \core\event\course_updated::create(array( 'objectid' => $course->id, 'context' => context_course::instance($course->id), 'other' => array('shortname' => $dbcourse->shortname, 'fullname' => $dbcourse->fullname, 'updatedfields' => array('category' => $category->id)) )); $event->trigger(); } fix_course_sortorder(); cache_helper::purge_by_event('changesincourse'); return true; } /** * Returns the display name of the given section that the course prefers * * Implementation of this function is provided by course format * @see core_courseformat\base::get_section_name() * * @param int|stdClass $courseorid The course to get the section name for (object or just course id) * @param int|stdClass|section_info $section Section object from database or just field course_sections.section * @return string Display name that the course format prefers, e.g. "Week 2" */ function get_section_name($courseorid, $section) { return course_get_format($courseorid)->get_section_name($section); } /** * Tells if current course format uses sections * * @param string $format Course format ID e.g. 'weeks' $course->format * @return bool */ function course_format_uses_sections($format) { $course = new stdClass(); $course->format = $format; return course_get_format($course)->uses_sections(); } /** * Returns the information about the ajax support in the given source format * * The returned object's property (boolean)capable indicates that * the course format supports Moodle course ajax features. * * @deprecated since Moodle 5.0 MDL-82351 * @todo MDL-83417 Remove this function in Moodle 6.0 * @param string $format * @return stdClass */ #[\core\attribute\deprecated(since: '5.0', mdl: 'MDL-82351')] function course_format_ajax_support($format) { \core\deprecation::emit_deprecation(__FUNCTION__); $course = new stdClass(); $course->format = $format; return course_get_format($course)->supports_ajax(); } /** * Can the current user delete this course? * Course creators have exception, * 1 day after the creation they can sill delete the course. * @param int $courseid * @return boolean */ function can_delete_course($courseid) { global $USER; $context = context_course::instance($courseid); if (has_capability('moodle/course:delete', $context)) { return true; } // hack: now try to find out if creator created this course recently (1 day) if (!has_capability('moodle/course:create', $context)) { return false; } $since = time() - 60*60*24; $course = get_course($courseid); if ($course->timecreated < $since) { return false; // Return if the course was not created in last 24 hours. } $logmanger = get_log_manager(); $readers = $logmanger->get_readers('\core\log\sql_reader'); $reader = reset($readers); if (empty($reader)) { return false; // No log reader found. } // A proper reader. $select = "userid = :userid AND courseid = :courseid AND eventname = :eventname AND timecreated > :since"; $params = array('userid' => $USER->id, 'since' => $since, 'courseid' => $course->id, 'eventname' => '\core\event\course_created'); return (bool)$reader->get_events_select_count($select, $params); } /** * Save the Your name for 'Some role' strings. * * @param integer $courseid the id of this course. * @param array|stdClass $data the data that came from the course settings form. */ function save_local_role_names($courseid, $data) { global $DB; $context = context_course::instance($courseid); foreach ($data as $fieldname => $value) { if (strpos($fieldname, 'role_') !== 0) { continue; } list($ignored, $roleid) = explode('_', $fieldname); // make up our mind whether we want to delete, update or insert if (!$value) { $DB->delete_records('role_names', array('contextid' => $context->id, 'roleid' => $roleid)); } else if ($rolename = $DB->get_record('role_names', array('contextid' => $context->id, 'roleid' => $roleid))) { $rolename->name = $value; $DB->update_record('role_names', $rolename); } else { $rolename = new stdClass; $rolename->contextid = $context->id; $rolename->roleid = $roleid; $rolename->name = $value; $DB->insert_record('role_names', $rolename); } // This will ensure the course contacts cache is purged.. core_course_category::role_assignment_changed($roleid, $context); } } /** * Returns options to use in course overviewfiles filemanager * * @param null|stdClass|core_course_list_element|int $course either object that has 'id' property or just the course id; * may be empty if course does not exist yet (course create form) * @return array|null array of options such as maxfiles, maxbytes, accepted_types, etc. * or null if overviewfiles are disabled */ function course_overviewfiles_options($course) { global $CFG; if (empty($CFG->courseoverviewfileslimit)) { return null; } // Create accepted file types based on config value, falling back to default all. $acceptedtypes = (new \core_form\filetypes_util)->normalize_file_types($CFG->courseoverviewfilesext); if (in_array('*', $acceptedtypes) || empty($acceptedtypes)) { $acceptedtypes = '*'; } $options = array( 'maxfiles' => $CFG->courseoverviewfileslimit, 'maxbytes' => $CFG->maxbytes, 'subdirs' => 0, 'accepted_types' => $acceptedtypes ); if (!empty($course->id)) { $options['context'] = context_course::instance($course->id); } else if (is_int($course) && $course > 0) { $options['context'] = context_course::instance($course); } return $options; } /** * Create a course and either return a $course object * * Please note this functions does not verify any access control, * the calling code is responsible for all validation (usually it is the form definition). * * @param array $editoroptions course description editor options * @param object $data - all the data needed for an entry in the 'course' table * @return object new course instance */ function create_course($data, $editoroptions = NULL) { global $DB, $CFG; //check the categoryid - must be given for all new courses $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST); // Check if the shortname already exists. if (!empty($data->shortname)) { if ($DB->record_exists('course', array('shortname' => $data->shortname))) { throw new moodle_exception('shortnametaken', '', '', $data->shortname); } } // Check if the idnumber already exists. if (!empty($data->idnumber)) { if ($DB->record_exists('course', array('idnumber' => $data->idnumber))) { throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber); } } if (empty($CFG->enablecourserelativedates)) { // Make sure we're not setting the relative dates mode when the setting is disabled. unset($data->relativedatesmode); } if ($errorcode = course_validate_dates((array)$data)) { throw new moodle_exception($errorcode); } // Check if timecreated is given. $data->timecreated = !empty($data->timecreated) ? $data->timecreated : time(); $data->timemodified = $data->timecreated; // place at beginning of any category $data->sortorder = 0; if ($editoroptions) { // summary text is updated later, we need context to store the files first $data->summary = ''; $data->summary_format = $data->summary_editor['format']; } // Get default completion settings as a fallback in case the enablecompletion field is not set. $courseconfig = get_config('moodlecourse'); $defaultcompletion = !empty($CFG->enablecompletion) ? $courseconfig->enablecompletion : COMPLETION_DISABLED; $enablecompletion = $data->enablecompletion ?? $defaultcompletion; // Unset showcompletionconditions when completion tracking is not enabled for the course. if ($enablecompletion == COMPLETION_DISABLED) { unset($data->showcompletionconditions); } else if (!isset($data->showcompletionconditions)) { // Show completion conditions should have a default value when completion is enabled. Set it to the site defaults. // This scenario can happen when a course is created through data generators or through a web service. $data->showcompletionconditions = $courseconfig->showcompletionconditions; } if (!isset($data->visible)) { // data not from form, add missing visibility info $data->visible = $category->visible; } $data->visibleold = $data->visible; $newcourseid = $DB->insert_record('course', $data); $context = context_course::instance($newcourseid, MUST_EXIST); if ($editoroptions) { // Save the files used in the summary editor and store $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0); $DB->set_field('course', 'summary', $data->summary, array('id'=>$newcourseid)); $DB->set_field('course', 'summaryformat', $data->summary_format, array('id'=>$newcourseid)); } if ($overviewfilesoptions = course_overviewfiles_options($newcourseid)) { // Save the course overviewfiles $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0); } // update course format options course_get_format($newcourseid)->update_course_format_options($data); $course = course_get_format($newcourseid)->get_course(); fix_course_sortorder(); // purge appropriate caches in case fix_course_sortorder() did not change anything cache_helper::purge_by_event('changesincourse'); // Trigger a course created event. $event = \core\event\course_created::create(array( 'objectid' => $course->id, 'context' => $context, 'other' => array('shortname' => $course->shortname, 'fullname' => $course->fullname) )); $event->trigger(); $data->id = $newcourseid; // Dispatch the hook for post course create actions. di::get(hook\manager::class)->dispatch( new \core_course\hook\after_course_created( course: $data, ), ); // Setup the blocks blocks_add_default_course_blocks($course); // Create default section and initial sections if specified (unless they've already been created earlier). // We do not want to call course_create_sections_if_missing() because to avoid creating course cache. $numsections = isset($data->numsections) ? $data->numsections : 0; $existingsections = $DB->get_fieldset_sql('SELECT section from {course_sections} WHERE course = ?', [$newcourseid]); $newsections = array_diff(range(0, $numsections), $existingsections); foreach ($newsections as $sectionnum) { course_create_section($newcourseid, $sectionnum, true); } // Save any custom role names. save_local_role_names($course->id, (array)$data); // set up enrolments enrol_course_updated(true, $course, $data); // Update course tags. if (isset($data->tags)) { core_tag_tag::set_item_tags('core', 'course', $course->id, $context, $data->tags); } // Save custom fields if there are any of them in the form. $handler = core_course\customfield\course_handler::create(); // Make sure to set the handler's parent context first. $coursecatcontext = context_coursecat::instance($category->id); $handler->set_parent_context($coursecatcontext); // Save the custom field data. $data->id = $course->id; $handler->instance_form_save($data, true); di::get(hook\manager::class)->dispatch( new \core_course\hook\after_form_submission($data, true), ); return $course; } /** * Update a course. * * Please note this functions does not verify any access control, * the calling code is responsible for all validation (usually it is the form definition). * * @param object $data - all the data needed for an entry in the 'course' table * @param array $editoroptions course description editor options * @return void */ function update_course($data, $editoroptions = NULL) { global $DB, $CFG; // Prevent changes on front page course. if ($data->id == SITEID) { throw new moodle_exception('invalidcourse', 'error'); } $oldcourse = course_get_format($data->id)->get_course(); $context = context_course::instance($oldcourse->id); // Make sure we're not changing whatever the course's relativedatesmode setting is. unset($data->relativedatesmode); // Capture the updated fields for the log data. $updatedfields = []; foreach (get_object_vars($oldcourse) as $field => $value) { if ($field == 'summary_editor') { if (($data->$field)['text'] !== $value['text']) { // The summary might be very long, we don't wan't to fill up the log record with the full text. $updatedfields[$field] = '(updated)'; } } else if ($field == 'tags' && isset($data->tags)) { // Tags might not have the same array keys, just check the values. if (array_values($data->$field) !== array_values($value)) { $updatedfields[$field] = $data->$field; } } else { if (isset($data->$field) && $data->$field != $value) { $updatedfields[$field] = $data->$field; } } } $data->timemodified = time(); if ($editoroptions) { $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0); } if ($overviewfilesoptions = course_overviewfiles_options($data->id)) { $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0); } // Check we don't have a duplicate shortname. if (!empty($data->shortname) && $oldcourse->shortname != $data->shortname) { if ($DB->record_exists_sql('SELECT id from {course} WHERE shortname = ? AND id <> ?', array($data->shortname, $data->id))) { throw new moodle_exception('shortnametaken', '', '', $data->shortname); } } // Check we don't have a duplicate idnumber. if (!empty($data->idnumber) && $oldcourse->idnumber != $data->idnumber) { if ($DB->record_exists_sql('SELECT id from {course} WHERE idnumber = ? AND id <> ?', array($data->idnumber, $data->id))) { throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber); } } if ($errorcode = course_validate_dates((array)$data)) { throw new moodle_exception($errorcode); } if (!isset($data->category) or empty($data->category)) { // prevent nulls and 0 in category field unset($data->category); } $changesincoursecat = $movecat = (isset($data->category) and $oldcourse->category != $data->category); if (!isset($data->visible)) { // data not from form, add missing visibility info $data->visible = $oldcourse->visible; } if ($data->visible != $oldcourse->visible) { // reset the visibleold flag when manually hiding/unhiding course $data->visibleold = $data->visible; $changesincoursecat = true; } else { if ($movecat) { $newcategory = $DB->get_record('course_categories', array('id'=>$data->category)); if (empty($newcategory->visible)) { // make sure when moving into hidden category the course is hidden automatically $data->visible = 0; } } } // Set newsitems to 0 if format does not support announcements. if (isset($data->format)) { $newcourseformat = course_get_format((object)['format' => $data->format]); if (!$newcourseformat->supports_news()) { $data->newsitems = 0; } } // Set showcompletionconditions to null when completion tracking has been disabled for the course. if (isset($data->enablecompletion) && $data->enablecompletion == COMPLETION_DISABLED) { $data->showcompletionconditions = null; } // Update custom fields if there are any of them in the form. $handler = core_course\customfield\course_handler::create(); $handler->instance_form_save($data); di::get(hook\manager::class)->dispatch( new \core_course\hook\after_form_submission($data), ); // Update with the new data $DB->update_record('course', $data); // make sure the modinfo cache is reset rebuild_course_cache($data->id); // Purge course image cache in case if course image has been updated. \cache::make('core', 'course_image')->delete($data->id); // update course format options with full course data course_get_format($data->id)->update_course_format_options($data, $oldcourse); $course = $DB->get_record('course', array('id'=>$data->id)); if ($movecat) { $newparent = context_coursecat::instance($course->category); $context->update_moved($newparent); } $fixcoursesortorder = $movecat || (isset($data->sortorder) && ($oldcourse->sortorder != $data->sortorder)); if ($fixcoursesortorder) { fix_course_sortorder(); } // purge appropriate caches in case fix_course_sortorder() did not change anything cache_helper::purge_by_event('changesincourse'); if ($changesincoursecat) { cache_helper::purge_by_event('changesincoursecat'); } // Test for and remove blocks which aren't appropriate anymore blocks_remove_inappropriate($course); // Save any custom role names. save_local_role_names($course->id, $data); // update enrol settings enrol_course_updated(false, $course, $data); // Update course tags. if (isset($data->tags)) { core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags); } // Trigger a course updated event. $event = \core\event\course_updated::create(array( 'objectid' => $course->id, 'context' => context_course::instance($course->id), 'other' => array('shortname' => $course->shortname, 'fullname' => $course->fullname, 'updatedfields' => $updatedfields) )); $event->trigger(); // Dispatch the hook for post course update actions. $hook = new \core_course\hook\after_course_updated( course: $data, oldcourse: $oldcourse, changeincoursecat: $changesincoursecat, ); \core\di::get(\core\hook\manager::class)->dispatch($hook); if ($oldcourse->format !== $course->format) { // Remove all options stored for the previous format // We assume that new course format migrated everything it needed watching trigger // 'course_updated' and in method format_XXX::update_course_format_options() $DB->delete_records('course_format_options', array('courseid' => $course->id, 'format' => $oldcourse->format)); } // Delete theme usage cache if the theme has been changed. if (isset($data->theme) && ($data->theme != $oldcourse->theme)) { theme_delete_used_in_context_cache($data->theme, $oldcourse->theme); } } /** * Calculate the average number of enrolled participants per course. * * This is intended for statistics purposes during the site registration. Only visible courses are taken into account. * Front page enrolments are excluded. * * @param bool $onlyactive Consider only active enrolments in enabled plugins and obey the enrolment time restrictions. * @param int $lastloginsince If specified, count only users who logged in after this timestamp. * @return float */ function average_number_of_participants(bool $onlyactive = false, ?int $lastloginsince = null): float { global $DB; $params = []; $sql = "SELECT DISTINCT ue.userid, e.courseid FROM {user_enrolments} ue JOIN {enrol} e ON e.id = ue.enrolid JOIN {course} c ON c.id = e.courseid "; if ($onlyactive || $lastloginsince) { $sql .= "JOIN {user} u ON u.id = ue.userid "; } $sql .= "WHERE e.courseid <> " . SITEID . " AND c.visible = 1 "; if ($onlyactive) { $sql .= "AND ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2) "; // Same as in the enrollib - the rounding should help caching in the database. $now = round(time(), -2); $params += [ 'active' => ENROL_USER_ACTIVE, 'enabled' => ENROL_INSTANCE_ENABLED, 'now1' => $now, 'now2' => $now, ]; } if ($lastloginsince) { $sql .= "AND u.lastlogin > :lastlogin "; $params['lastlogin'] = $lastloginsince; } $sql = "SELECT COUNT(*) FROM ($sql) total"; $enrolmenttotal = $DB->count_records_sql($sql, $params); // Get the number of visible courses (exclude the front page). $coursetotal = $DB->count_records('course', ['visible' => 1]); $coursetotal = $coursetotal - 1; if (empty($coursetotal)) { $participantaverage = 0; } else { $participantaverage = $enrolmenttotal / $coursetotal; } return $participantaverage; } /** * Average number of course modules * @return integer */ function average_number_of_courses_modules() { global $DB, $SITE; //count total of visible course module (except front page) $sql = 'SELECT COUNT(*) FROM ( SELECT cm.course, cm.module FROM {course} c, {course_modules} cm WHERE c.id = cm.course AND c.id <> :siteid AND cm.visible = 1 AND c.visible = 1) total'; $params = array('siteid' => $SITE->id); $moduletotal = $DB->count_records_sql($sql, $params); //count total of visible courses (minus front page) $coursetotal = $DB->count_records('course', array('visible' => 1)); $coursetotal = $coursetotal - 1 ; //average of course module if (empty($coursetotal)) { $coursemoduleaverage = 0; } else { $coursemoduleaverage = $moduletotal / $coursetotal; } return $coursemoduleaverage; } /** * This class pertains to course requests and contains methods associated with * create, approving, and removing course requests. * * Please note we do not allow embedded images here because there is no context * to store them with proper access control. * * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 * * @property-read int $id * @property-read string $fullname * @property-read string $shortname * @property-read string $summary * @property-read int $summaryformat * @property-read int $summarytrust * @property-read string $reason * @property-read int $requester */ class course_request { /** * This is the stdClass that stores the properties for the course request * and is externally accessed through the __get magic method * @var stdClass */ protected $properties; /** * An array of options for the summary editor used by course request forms. * This is initially set by {@link summary_editor_options()} * @var array * @static */ protected static $summaryeditoroptions; /** * Static function to prepare the summary editor for working with a course * request. * * @static * @param null|stdClass $data Optional, an object containing the default values * for the form, these may be modified when preparing the * editor so this should be called before creating the form * @return stdClass An object that can be used to set the default values for * an mforms form */ public static function prepare($data=null) { if ($data === null) { $data = new stdClass; } $data = file_prepare_standard_editor($data, 'summary', self::summary_editor_options()); return $data; } /** * Static function to create a new course request when passed an array of properties * for it. * * This function also handles saving any files that may have been used in the editor * * @static * @param stdClass $data * @return course_request The newly created course request */ public static function create($data) { global $USER, $DB, $CFG; $data->requester = $USER->id; // Setting the default category if none set. if (empty($data->category) || !empty($CFG->lockrequestcategory)) { $data->category = $CFG->defaultrequestcategory; } // Summary is a required field so copy the text over $data->summary = $data->summary_editor['text']; $data->summaryformat = $data->summary_editor['format']; $data->id = $DB->insert_record('course_request', $data); // Create a new course_request object and return it $request = new course_request($data); // Notify the admin if required. if ($users = get_users_from_config($CFG->courserequestnotify, 'moodle/site:approvecourse')) { $a = new stdClass; $a->link = "$CFG->wwwroot/course/pending.php"; $a->user = fullname($USER); $subject = get_string('courserequest'); $message = get_string('courserequestnotifyemail', 'admin', $a); foreach ($users as $user) { $request->notify($user, $USER, 'courserequested', $subject, $message); } } return $request; } /** * Returns an array of options to use with a summary editor * * @uses course_request::$summaryeditoroptions * @return array An array of options to use with the editor */ public static function summary_editor_options() { global $CFG; if (self::$summaryeditoroptions === null) { self::$summaryeditoroptions = array('maxfiles' => 0, 'maxbytes'=>0); } return self::$summaryeditoroptions; } /** * Loads the properties for this course request object. Id is required and if * only id is provided then we load the rest of the properties from the database * * @param stdClass|int $properties Either an object containing properties * or the course_request id to load */ public function __construct($properties) { global $DB; if (empty($properties->id)) { if (empty($properties)) { throw new coding_exception('You must provide a course request id when creating a course_request object'); } $id = $properties; $properties = new stdClass; $properties->id = (int)$id; unset($id); } if (empty($properties->requester)) { if (!($this->properties = $DB->get_record('course_request', array('id' => $properties->id)))) { throw new \moodle_exception('unknowncourserequest'); } } else { $this->properties = $properties; } $this->properties->collision = null; } /** * Returns the requested property * * @param string $key * @return mixed */ public function __get($key) { return $this->properties->$key; } /** * Override this to ensure empty($request->blah) calls return a reliable answer... * * This is required because we define the __get method * * @param mixed $key * @return bool True is it not empty, false otherwise */ public function __isset($key) { return (!empty($this->properties->$key)); } /** * Returns the user who requested this course * * Uses a static var to cache the results and cut down the number of db queries * * @staticvar array $requesters An array of cached users * @return stdClass The user who requested the course */ public function get_requester() { global $DB; static $requesters= array(); if (!array_key_exists($this->properties->requester, $requesters)) { $requesters[$this->properties->requester] = $DB->get_record('user', array('id'=>$this->properties->requester)); } return $requesters[$this->properties->requester]; } /** * Checks that the shortname used by the course does not conflict with any other * courses that exist * * @param string|null $shortnamemark The string to append to the requests shortname * should a conflict be found * @return bool true is there is a conflict, false otherwise */ public function check_shortname_collision($shortnamemark = '[*]') { global $DB; if ($this->properties->collision !== null) { return $this->properties->collision; } if (empty($this->properties->shortname)) { debugging('Attempting to check a course request shortname before it has been set', DEBUG_DEVELOPER); $this->properties->collision = false; } else if ($DB->record_exists('course', array('shortname' => $this->properties->shortname))) { if (!empty($shortnamemark)) { $this->properties->shortname .= ' '.$shortnamemark; } $this->properties->collision = true; } else { $this->properties->collision = false; } return $this->properties->collision; } /** * Checks user capability to approve a requested course * * If course was requested without category for some reason (might happen if $CFG->defaultrequestcategory is * misconfigured), we check capabilities 'moodle/site:approvecourse' and 'moodle/course:changecategory'. * * @return bool */ public function can_approve() { global $CFG; $category = null; if ($this->properties->category) { $category = core_course_category::get($this->properties->category, IGNORE_MISSING); } else if ($CFG->defaultrequestcategory) { $category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING); } if ($category) { return has_capability('moodle/site:approvecourse', $category->get_context()); } // We can not determine the context where the course should be created. The approver should have // both capabilities to approve courses and change course category in the system context. return has_all_capabilities(['moodle/site:approvecourse', 'moodle/course:changecategory'], context_system::instance()); } /** * Returns the category where this course request should be created * * Note that we don't check here that user has a capability to view * hidden categories if he has capabilities 'moodle/site:approvecourse' and * 'moodle/course:changecategory' * * @return core_course_category */ public function get_category() { global $CFG; if ($this->properties->category && ($category = core_course_category::get($this->properties->category, IGNORE_MISSING))) { return $category; } else if ($CFG->defaultrequestcategory && ($category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING))) { return $category; } else { return core_course_category::get_default(); } } /** * This function approves the request turning it into a course * * This function converts the course request into a course, at the same time * transferring any files used in the summary to the new course and then removing * the course request and the files associated with it. * * @return int The id of the course that was created from this request */ public function approve() { global $CFG, $DB, $USER; require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); $user = $DB->get_record('user', array('id' => $this->properties->requester, 'deleted'=>0), '*', MUST_EXIST); $courseconfig = get_config('moodlecourse'); // Transfer appropriate settings $data = clone($this->properties); unset($data->id); unset($data->reason); unset($data->requester); // Set category $category = $this->get_category(); $data->category = $category->id; // Set misc settings $data->requested = 1; // Apply course default settings $data->format = $courseconfig->format; $data->newsitems = $courseconfig->newsitems; $data->showgrades = $courseconfig->showgrades; $data->showreports = $courseconfig->showreports; $data->showactivitydates = $courseconfig->showactivitydates; $data->maxbytes = $courseconfig->maxbytes; $data->groupmode = $courseconfig->groupmode; $data->groupmodeforce = $courseconfig->groupmodeforce; $data->visible = $courseconfig->visible; $data->visibleold = $data->visible; $data->lang = $courseconfig->lang; $data->enablecompletion = $courseconfig->enablecompletion; $data->numsections = $courseconfig->numsections; $data->startdate = usergetmidnight(di::get(clock::class)->time()); if ($courseconfig->courseenddateenabled) { $data->enddate = $data->startdate + $courseconfig->courseduration; } list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname); $course = create_course($data); $context = context_course::instance($course->id, MUST_EXIST); // add enrol instances if (!$DB->record_exists('enrol', array('courseid'=>$course->id, 'enrol'=>'manual'))) { if ($manual = enrol_get_plugin('manual')) { $manual->add_default_instance($course); } } // enrol the requester as teacher if necessary if (!empty($CFG->creatornewroleid) and !is_viewing($context, $user, 'moodle/role:assign') and !is_enrolled($context, $user, 'moodle/role:assign')) { enrol_try_internal_enrol($course->id, $user->id, $CFG->creatornewroleid); } $this->delete(); $a = new stdClass(); $a->name = format_string($course->fullname, true, ['context' => $context]); $a->url = course_get_url($course); $usernameplaceholders = \core\user::get_name_placeholders($user); foreach ($usernameplaceholders as $field => $value) { $a->{$field} = $value; } $this->notify($user, $USER, 'courserequestapproved', get_string('courseapprovedsubject'), get_string('courseapprovedemail2', 'moodle', $a), $course->id); return $course->id; } /** * Reject a course request * * This function rejects a course request, emailing the requesting user the * provided notice and then removing the request from the database * * @param string $notice The message to display to the user */ public function reject($notice) { global $USER, $DB; $user = $DB->get_record('user', array('id' => $this->properties->requester), '*', MUST_EXIST); $this->notify($user, $USER, 'courserequestrejected', get_string('courserejectsubject'), get_string('courserejectemail', 'moodle', $notice)); $this->delete(); } /** * Deletes the course request and any associated files */ public function delete() { global $DB; $DB->delete_records('course_request', array('id' => $this->properties->id)); } /** * Send a message from one user to another using events_trigger * * @param object $touser * @param object $fromuser * @param string $name * @param string $subject * @param string $message * @param int|null $courseid */ protected function notify($touser, $fromuser, $name, $subject, $message, $courseid = null) { $eventdata = new \core\message\message(); $eventdata->courseid = empty($courseid) ? SITEID : $courseid; $eventdata->component = 'moodle'; $eventdata->name = $name; $eventdata->userfrom = $fromuser; $eventdata->userto = $touser; $eventdata->subject = $subject; $eventdata->fullmessage = $message; $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = ''; $eventdata->smallmessage = ''; $eventdata->notification = 1; message_send($eventdata); } /** * Checks if current user can request a course in this context * * @param context $context * @return bool */ public static function can_request(context $context) { global $CFG; if (empty($CFG->enablecourserequests)) { return false; } if (has_capability('moodle/course:create', $context)) { return false; } if ($context instanceof context_system) { $defaultcontext = context_coursecat::instance($CFG->defaultrequestcategory, IGNORE_MISSING); return $defaultcontext && has_capability('moodle/course:request', $defaultcontext); } else if ($context instanceof context_coursecat) { if (!$CFG->lockrequestcategory || $CFG->defaultrequestcategory == $context->instanceid) { return has_capability('moodle/course:request', $context); } } return false; } } /** * Return a list of page types * @param string $pagetype current page type * @param context $parentcontext Block's parent context * @param context $currentcontext Current context of block * @return array array of page types */ function course_page_type_list($pagetype, $parentcontext, $currentcontext) { if ($pagetype === 'course-index' || $pagetype === 'course-index-category') { // For courses and categories browsing pages (/course/index.php) add option to show on ANY category page $pagetypes = array('*' => get_string('page-x', 'pagetype'), 'course-index-*' => get_string('page-course-index-x', 'pagetype'), ); } else if ($currentcontext && (!($coursecontext = $currentcontext->get_course_context(false)) || $coursecontext->instanceid == SITEID)) { // We know for sure that despite pagetype starts with 'course-' this is not a page in course context (i.e. /course/search.php, etc.) $pagetypes = array('*' => get_string('page-x', 'pagetype')); } else { // Otherwise consider it a page inside a course even if $currentcontext is null $pagetypes = array('*' => get_string('page-x', 'pagetype'), 'course-*' => get_string('page-course-x', 'pagetype'), 'course-view-*' => get_string('page-course-view-x', 'pagetype') ); } return $pagetypes; } /** * Determine whether course ajax should be enabled for the specified course * * @param stdClass $course The course to test against * @return boolean Whether course ajax is enabled or note */ function course_ajax_enabled($course) { global $CFG, $PAGE, $SITE; // The user must be editing for AJAX to be included if (!$PAGE->user_is_editing()) { return false; } // Check that the theme suports if (!$PAGE->theme->enablecourseajax) { return false; } // Check that the course format supports ajax functionality // The site 'format' doesn't have information on course format support if ($SITE->id !== $course->id) { $courseformatajaxsupport = course_get_format($course)->supports_ajax(); if (!$courseformatajaxsupport->capable) { return false; } } // All conditions have been met so course ajax should be enabled return true; } /** * Include the relevant javascript and language strings for the resource * toolbox YUI module * * @param integer $id The ID of the course being applied to * @param array $usedmodules An array containing the names of the modules in use on the page * @param array $enabledmodules An array containing the names of the enabled (visible) modules on this site * @param stdClass $config An object containing configuration parameters for ajax modules including: * * resourceurl The URL to post changes to for resource changes * * sectionurl The URL to post changes to for section changes * * pageparams Additional parameters to pass through in the post * @return bool */ function include_course_ajax($course, $usedmodules = [], $enabledmodules = null, $config = null) { global $CFG, $PAGE, $SITE; // Init the course editor module to support UI components. $format = course_get_format($course); include_course_editor($format); // TODO remove this if as part of MDL-83627. // Ensure that ajax should be included if (!course_ajax_enabled($course)) { return false; } // TODO remove this if as part of MDL-83627. // Component based formats don't use YUI drag and drop anymore. if (!$format->supports_components() && course_format_uses_sections($course->format)) { debugging( 'The old course editor will be removed in Moodle 6.0. Ensure your format return true to supports_components', DEBUG_DEVELOPER ); if (!$config) { $config = new stdClass(); } // The URL to use for resource changes. if (!isset($config->resourceurl)) { $config->resourceurl = '/course/rest.php'; } // The URL to use for section changes. if (!isset($config->sectionurl)) { $config->sectionurl = '/course/rest.php'; } // Any additional parameters which need to be included on page submission. if (!isset($config->pageparams)) { $config->pageparams = array(); } $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop', array(array( 'courseid' => $course->id, 'ajaxurl' => $config->sectionurl, 'config' => $config, )), null, true); $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_resource_dragdrop', array(array( 'courseid' => $course->id, 'ajaxurl' => $config->resourceurl, 'config' => $config, )), null, true); // Require various strings for the command toolbox. $PAGE->requires->strings_for_js(array( 'moveleft', 'deletechecktype', 'deletechecktypename', 'edittitle', 'edittitleinstructions', 'show', 'hide', 'highlight', 'highlightoff', 'groupsnone', 'groupsvisible', 'groupsseparate', 'movesection', 'movecoursemodule', 'movecoursesection', 'movecontent', 'tocontent', 'emptydragdropregion', 'afterresource', 'aftersection', 'totopofsection', ), 'moodle'); // Include section-specific strings for formats which support sections. if (course_format_uses_sections($course->format)) { $PAGE->requires->strings_for_js(array( 'showfromothers', 'hidefromothers', ), 'format_' . $course->format); } // For confirming resource deletion we need the name of the module in question. foreach ($usedmodules as $module => $modname) { $PAGE->requires->string_for_js('pluginname', $module); } // Load drag and drop upload AJAX. require_once($CFG->dirroot.'/course/dnduploadlib.php'); dndupload_add_to_course($course, $enabledmodules); $PAGE->requires->js_call_amd('core_course/actions', 'initCoursePage', [$course->format]); } return true; } /** * Include and configure the course editor modules. * * @param course_format $format the course format instance. */ function include_course_editor(course_format $format) { global $PAGE, $SITE; $course = $format->get_course(); if (!$format->supports_ajax()?->capable) { return; } $statekey = course_format::session_cache($course); // Edition mode and some format specs must be passed to the init method. $setup = (object)[ 'editing' => $format->show_editor(), 'supportscomponents' => $format->supports_components(), 'statekey' => $statekey, 'overriddenStrings' => $format->get_editor_custom_strings(), ]; // All the new editor elements will be loaded after the course is presented and // the initial course state will be generated using core_course_get_state webservice. $PAGE->requires->js_call_amd('core_courseformat/courseeditor', 'setViewFormat', [$course->id, $setup]); } /** * Returns the sorted list of available course formats, filtered by enabled if necessary * * @param bool $enabledonly return only formats that are enabled * @return array array of sorted format names */ function get_sorted_course_formats($enabledonly = false) { global $CFG; // Include both formats that exist on disk (but might not have been installed yet), and those // which were installed but no longer exist on disk. $installedformats = core_plugin_manager::instance()->get_installed_plugins('format'); $existingformats = core_component::get_plugin_list('format'); $formats = array_merge($installedformats, $existingformats); if (!empty($CFG->format_plugins_sortorder)) { $order = explode(',', $CFG->format_plugins_sortorder); $order = array_merge(array_intersect($order, array_keys($formats)), array_diff(array_keys($formats), $order)); } else { $order = array_keys($formats); } if (!$enabledonly) { return $order; } $sortedformats = array(); foreach ($order as $formatname) { $component = "format_{$formatname}"; $componentdir = core_component::get_component_directory($component); if ($componentdir !== null && !get_config($component, 'disabled')) { $sortedformats[] = $formatname; } } return $sortedformats; } /** * The URL to use for the specified course (with section) * * @param int|stdClass $courseorid The course to get the section name for (either object or just course id) * @param int|stdClass $section Section object from database or just field course_sections.section * if omitted the course view page is returned * @param array $options options for view URL. At the moment core uses: * 'navigation' (bool) if true and section has no separate page, the function returns null * 'sr' (int) used by multipage formats to specify to which section to return * @return moodle_url|null The url of course */ function course_get_url($courseorid, $section = null, $options = array()) { return course_get_format($courseorid)->get_view_url($section, $options); } /** * Create a module. * * It includes: * - capability checks and other checks * - create the module from the module info * * @param object $module * @return object the created module info * @throws moodle_exception if user is not allowed to perform the action or module is not allowed in this course */ function create_module($moduleinfo) { global $DB, $CFG; require_once($CFG->dirroot . '/course/modlib.php'); // Check manadatory attributs. $mandatoryfields = array('modulename', 'course', 'section', 'visible'); if (plugin_supports('mod', $moduleinfo->modulename, FEATURE_MOD_INTRO, true)) { $mandatoryfields[] = 'introeditor'; } foreach($mandatoryfields as $mandatoryfield) { if (!isset($moduleinfo->{$mandatoryfield})) { throw new moodle_exception('createmodulemissingattribut', '', '', $mandatoryfield); } } // Some additional checks (capability / existing instances). $course = $DB->get_record('course', array('id'=>$moduleinfo->course), '*', MUST_EXIST); list($module, $context, $cw) = can_add_moduleinfo($course, $moduleinfo->modulename, $moduleinfo->section); // Add the module. $moduleinfo->module = $module->id; $moduleinfo = add_moduleinfo($moduleinfo, $course, null); return $moduleinfo; } /** * Update a module. * * It includes: * - capability and other checks * - update the module * * @param object $module * @return object the updated module info * @throws moodle_exception if current user is not allowed to update the module */ function update_module($moduleinfo) { global $DB, $CFG; require_once($CFG->dirroot . '/course/modlib.php'); // Check the course module exists. $cm = get_coursemodule_from_id('', $moduleinfo->coursemodule, 0, false, MUST_EXIST); // Check the course exists. $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST); // Some checks (capaibility / existing instances). list($cm, $context, $module, $data, $cw) = can_update_moduleinfo($cm); // Retrieve few information needed by update_moduleinfo. $moduleinfo->modulename = $cm->modname; if (!isset($moduleinfo->scale)) { $moduleinfo->scale = 0; } $moduleinfo->type = 'mod'; // Update the module. list($cm, $moduleinfo) = update_moduleinfo($cm, $moduleinfo, $course, null); return $moduleinfo; } /** * Duplicate a module on the course for ajax. * * @see mod_duplicate_module() * @param object $course The course * @param object $cm The course module to duplicate * @param int $sr The section to link back to (used for creating the links) * @throws moodle_exception if the plugin doesn't support duplication * @return stdClass Object containing: * - fullcontent: The HTML markup for the created CM * - cmid: The CMID of the newly created CM * - redirect: Whether to trigger a redirect following this change */ function mod_duplicate_activity($course, $cm, $sr = null) { global $PAGE; $newcm = duplicate_module($course, $cm); $resp = new stdClass(); if ($newcm) { $format = course_get_format($course); $renderer = $format->get_renderer($PAGE); $modinfo = $format->get_modinfo(); $section = $modinfo->get_section_info($newcm->sectionnum); // Get the new element html content. $resp->fullcontent = $renderer->course_section_updated_cm_item($format, $section, $newcm); $resp->cmid = $newcm->id; } else { // Trigger a redirect. $resp->redirect = true; } return $resp; } /** * Api to duplicate a module. * * @param object $course course object. * @param object $cm course module object to be duplicated. * @param int $sectionid section ID new course module will be placed in. * @param bool $changename updates module name with text from duplicatedmodule lang string. * @since Moodle 2.8 * * @throws Exception * @throws coding_exception * @throws moodle_exception * @throws restore_controller_exception * * @return cm_info|null cminfo object if we sucessfully duplicated the mod and found the new cm. */ function duplicate_module($course, $cm, ?int $sectionid = null, bool $changename = true): ?cm_info { global $CFG, $DB, $USER; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); require_once($CFG->libdir . '/filelib.php'); // Plugins with this feature flag set to false must ALWAYS be in section 0. if (!course_modinfo::is_mod_type_visible_on_course($cm->modname)) { if (get_fast_modinfo($course)->get_section_info(0, MUST_EXIST)->id != $sectionid) { throw new coding_exception('Modules with FEATURE_CAN_DISPLAY set to false can not be moved from section 0'); } } $a = new stdClass(); $a->modtype = get_string('modulename', $cm->modname); $a->modname = format_string($cm->name); if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) { throw new moodle_exception('duplicatenosupport', 'error', '', $a); } // Backup the activity. $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cm->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $backupid = $bc->get_backupid(); $backupbasepath = $bc->get_plan()->get_basepath(); $bc->execute_plan(); $bc->destroy(); // Restore the backup immediately. $rc = new restore_controller($backupid, $course->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING); // Make sure that the restore_general_groups setting is always enabled when duplicating an activity. $plan = $rc->get_plan(); $groupsetting = $plan->get_setting('groups'); if (empty($groupsetting->get_value())) { $groupsetting->set_value(true); } $cmcontext = context_module::instance($cm->id); if (!$rc->execute_precheck()) { $precheckresults = $rc->get_precheck_results(); if (is_array($precheckresults) && !empty($precheckresults['errors'])) { if (empty($CFG->keeptempdirectoriesonbackup)) { fulldelete($backupbasepath); } } } $rc->execute_plan(); // Now a bit hacky part follows - we try to get the cmid of the newly // restored copy of the module. $newcmid = null; $tasks = $rc->get_plan()->get_tasks(); foreach ($tasks as $task) { if (is_subclass_of($task, 'restore_activity_task')) { if ($task->get_old_contextid() == $cmcontext->id) { $newcmid = $task->get_moduleid(); break; } } } $rc->destroy(); if (empty($CFG->keeptempdirectoriesonbackup)) { fulldelete($backupbasepath); } // If we know the cmid of the new course module, let us move it // right below the original one. otherwise it will stay at the // end of the section. if ($newcmid) { // Proceed with activity renaming before everything else. We don't use APIs here to avoid // triggering a lot of create/update duplicated events. $newcm = get_coursemodule_from_id($cm->modname, $newcmid, $cm->course); if ($changename) { // Add ' (copy)' language string postfix to duplicated module. $newname = get_string('duplicatedmodule', 'moodle', $newcm->name); set_coursemodule_name($newcm->id, $newname); } $section = $DB->get_record('course_sections', ['id' => $sectionid ?? $cm->section, 'course' => $cm->course]); if (isset($sectionid)) { moveto_module($newcm, $section); } else { $modarray = explode(",", trim($section->sequence)); $cmindex = array_search($cm->id, $modarray); if ($cmindex !== false && $cmindex < count($modarray) - 1) { moveto_module($newcm, $section, $modarray[$cmindex + 1]); } } // Update calendar events with the duplicated module. // The following line is to be removed in MDL-58906. course_module_update_calendar_events($newcm->modname, null, $newcm); // Copy permission overrides to new course module. $newcmcontext = context_module::instance($newcm->id); $overrides = $DB->get_records('role_capabilities', ['contextid' => $cmcontext->id]); foreach ($overrides as $override) { $override->contextid = $newcmcontext->id; unset($override->id); $DB->insert_record('role_capabilities', $override); } // Copy locally assigned roles to new course module. $overrides = $DB->get_records('role_assignments', ['contextid' => $cmcontext->id]); foreach ($overrides as $override) { $override->contextid = $newcmcontext->id; unset($override->id); $DB->insert_record('role_assignments', $override); } // Trigger course module created event. We can trigger the event only if we know the newcmid. $newcm = get_fast_modinfo($cm->course)->get_cm($newcmid); $event = \core\event\course_module_created::create_from_cm($newcm); $event->trigger(); } return isset($newcm) ? $newcm : null; } /** * Compare two objects to find out their correct order based on timestamp (to be used by usort). * Sorts by descending order of time. * * @param stdClass $a First object * @param stdClass $b Second object * @return int 0,1,-1 representing the order */ function compare_activities_by_time_desc($a, $b) { // Make sure the activities actually have a timestamp property. if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) { return 0; } // We treat instances without timestamp as if they have a timestamp of 0. if ((!property_exists($a, 'timestamp')) && (property_exists($b,'timestamp'))) { return 1; } if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) { return -1; } if ($a->timestamp == $b->timestamp) { return 0; } return ($a->timestamp > $b->timestamp) ? -1 : 1; } /** * Compare two objects to find out their correct order based on timestamp (to be used by usort). * Sorts by ascending order of time. * * @param stdClass $a First object * @param stdClass $b Second object * @return int 0,1,-1 representing the order */ function compare_activities_by_time_asc($a, $b) { // Make sure the activities actually have a timestamp property. if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) { return 0; } // We treat instances without timestamp as if they have a timestamp of 0. if ((!property_exists($a, 'timestamp')) && (property_exists($b, 'timestamp'))) { return -1; } if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) { return 1; } if ($a->timestamp == $b->timestamp) { return 0; } return ($a->timestamp < $b->timestamp) ? -1 : 1; } /** * Changes the visibility of a course. * * @param int $courseid The course to change. * @param bool $show True to make it visible, false otherwise. * @return bool */ function course_change_visibility($courseid, $show = true) { $course = new stdClass; $course->id = $courseid; $course->visible = ($show) ? '1' : '0'; $course->visibleold = $course->visible; update_course($course); return true; } /** * Changes the course sortorder by one, moving it up or down one in respect to sort order. * * @param stdClass|core_course_list_element $course * @param bool $up If set to true the course will be moved up one. Otherwise down one. * @return bool */ function course_change_sortorder_by_one($course, $up) { global $DB; $params = array($course->sortorder, $course->category); if ($up) { $select = 'sortorder < ? AND category = ?'; $sort = 'sortorder DESC'; } else { $select = 'sortorder > ? AND category = ?'; $sort = 'sortorder ASC'; } fix_course_sortorder(); $swapcourse = $DB->get_records_select('course', $select, $params, $sort, '*', 0, 1); if ($swapcourse) { $swapcourse = reset($swapcourse); $DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id' => $course->id)); $DB->set_field('course', 'sortorder', $course->sortorder, array('id' => $swapcourse->id)); // Finally reorder courses. fix_course_sortorder(); cache_helper::purge_by_event('changesincourse'); return true; } return false; } /** * Changes the sort order of courses in a category so that the first course appears after the second. * * @param int|stdClass $courseorid The course to focus on. * @param int $moveaftercourseid The course to shifter after or 0 if you want it to be the first course in the category. * @return bool */ function course_change_sortorder_after_course($courseorid, $moveaftercourseid) { global $DB; if (!is_object($courseorid)) { $course = get_course($courseorid); } else { $course = $courseorid; } if ((int)$moveaftercourseid === 0) { // We've moving the course to the start of the queue. $sql = 'SELECT sortorder FROM {course} WHERE category = :categoryid ORDER BY sortorder'; $params = array( 'categoryid' => $course->category ); $sortorder = $DB->get_field_sql($sql, $params, IGNORE_MULTIPLE); $sql = 'UPDATE {course} SET sortorder = sortorder + 1 WHERE category = :categoryid AND id <> :id'; $params = array( 'categoryid' => $course->category, 'id' => $course->id, ); $DB->execute($sql, $params); $DB->set_field('course', 'sortorder', $sortorder, array('id' => $course->id)); } else if ($course->id === $moveaftercourseid) { // They're the same - moronic. debugging("Invalid move after course given.", DEBUG_DEVELOPER); return false; } else { // Moving this course after the given course. It could be before it could be after. $moveaftercourse = get_course($moveaftercourseid); if ($course->category !== $moveaftercourse->category) { debugging("Cannot re-order courses. The given courses do not belong to the same category.", DEBUG_DEVELOPER); return false; } // Increment all courses in the same category that are ordered after the moveafter course. // This makes a space for the course we're moving. $sql = 'UPDATE {course} SET sortorder = sortorder + 1 WHERE category = :categoryid AND sortorder > :sortorder'; $params = array( 'categoryid' => $moveaftercourse->category, 'sortorder' => $moveaftercourse->sortorder ); $DB->execute($sql, $params); $DB->set_field('course', 'sortorder', $moveaftercourse->sortorder + 1, array('id' => $course->id)); } fix_course_sortorder(); cache_helper::purge_by_event('changesincourse'); return true; } /** * Trigger course viewed event. This API function is used when course view actions happens, * usually in course/view.php but also in external functions. * * @param stdClass $context course context object * @param int $sectionnumber section number * @since Moodle 2.9 */ function course_view($context, $sectionnumber = 0) { $eventdata = array('context' => $context); if (!empty($sectionnumber)) { $eventdata['other']['coursesectionnumber'] = $sectionnumber; } $event = \core\event\course_viewed::create($eventdata); $event->trigger(); user_accesstime_log($context->instanceid); } /** * Returns courses tagged with a specified tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ function course_get_tagged_courses($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) { global $CFG, $PAGE; $perpage = $exclusivemode ? $CFG->coursesperpage : 5; $displayoptions = array( 'limit' => $perpage, 'offset' => $page * $perpage, 'viewmoreurl' => null, ); $courserenderer = $PAGE->get_renderer('core', 'course'); $totalcount = core_course_category::search_courses_count(array('tagid' => $tag->id, 'ctx' => $ctx, 'rec' => $rec)); $content = $courserenderer->tagged_courses($tag->id, $exclusivemode, $ctx, $rec, $displayoptions); $totalpages = ceil($totalcount / $perpage); return new core_tag\output\tagindex($tag, 'core', 'course', $content, $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); } /** * Implements callback inplace_editable() allowing to edit values in-place * * @param string $itemtype * @param int $itemid * @param mixed $newvalue * @return ?\core\output\inplace_editable */ function core_course_inplace_editable($itemtype, $itemid, $newvalue) { if ($itemtype === 'activityname') { return \core_courseformat\output\local\content\cm\title::update($itemid, $newvalue); } } /** * This function calculates the minimum and maximum cutoff values for the timestart of * the given event. * * It will return an array with two values, the first being the minimum cutoff value and * the second being the maximum cutoff value. Either or both values can be null, which * indicates there is no minimum or maximum, respectively. * * If a cutoff is required then the function must return an array containing the cutoff * timestamp and error string to display to the user if the cutoff value is violated. * * A minimum and maximum cutoff return value will look like: * [ * [1505704373, 'The date must be after this date'], * [1506741172, 'The date must be before this date'] * ] * * @param calendar_event $event The calendar event to get the time range for * @param stdClass $course The course object to get the range from * @return array Returns an array with min and max date. */ function core_course_core_calendar_get_valid_event_timestart_range(\calendar_event $event, $course) { $mindate = null; $maxdate = null; if ($course->startdate) { $mindate = [ $course->startdate, get_string('errorbeforecoursestart', 'calendar') ]; } return [$mindate, $maxdate]; } /** * Render the message drawer to be included in the top of the body of each page. * * @return string HTML */ function core_course_drawer(): string { global $PAGE; // If the course index is explicitly set and if it should be hidden. if ($PAGE->get_show_course_index() === false) { return ''; } // Only add course index on non-site course pages. if (!$PAGE->course || $PAGE->course->id == SITEID) { return ''; } // Show course index to users can access the course only. if (!can_access_course($PAGE->course, null, '', true)) { return ''; } $format = course_get_format($PAGE->course); $renderer = $format->get_renderer($PAGE); if (method_exists($renderer, 'course_index_drawer')) { return $renderer->course_index_drawer($format); } return ''; } /** * Returns course modules tagged with a specified tag ready for output on tag/index.php page * * This is a callback used by the tag area core/course_modules to search for course modules * tagged with a specific tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromcontextid context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $contextid context id where to search for records * @param bool $recursivecontext search in subcontexts as well * @param int $page 0-based number of page being displayed * @return ?\core_tag\output\tagindex */ function course_get_tagged_course_modules($tag, $exclusivemode = false, $fromcontextid = 0, $contextid = 0, $recursivecontext = 1, $page = 0) { global $OUTPUT; $perpage = $exclusivemode ? 20 : 5; // Build select query. $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); $query = "SELECT cm.id AS cmid, c.id AS courseid, $ctxselect FROM {course_modules} cm JOIN {tag_instance} tt ON cm.id = tt.itemid JOIN {course} c ON cm.course = c.id JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component AND cm.deletioninprogress = 0 AND c.id %COURSEFILTER% AND cm.id %ITEMFILTER%"; $params = array('itemtype' => 'course_modules', 'tagid' => $tag->id, 'component' => 'core', 'coursemodulecontextlevel' => CONTEXT_MODULE); if ($contextid) { $context = context::instance_by_id($contextid); $query .= $recursivecontext ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid'; $params['contextid'] = $context->id; $params['path'] = $context->path.'/%'; } $query .= ' ORDER BY'; if ($fromcontextid) { // In order-clause specify that modules from inside "fromctx" context should be returned first. $fromcontext = context::instance_by_id($fromcontextid); $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),'; $params['fromcontextid'] = $fromcontext->id; $params['frompath'] = $fromcontext->path.'/%'; } $query .= ' c.sortorder, cm.id'; $totalpages = $page + 1; // Use core_tag_index_builder to build and filter the list of items. // Request one item more than we need so we know if next page exists. $builder = new core_tag_index_builder('core', 'course_modules', $query, $params, $page * $perpage, $perpage + 1); while ($item = $builder->has_item_that_needs_access_check()) { context_helper::preload_from_record($item); $courseid = $item->courseid; if (!$builder->can_access_course($courseid)) { $builder->set_accessible($item, false); continue; } $modinfo = get_fast_modinfo($builder->get_course($courseid)); // Set accessibility of this item and all other items in the same course. $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) { if ($taggeditem->courseid == $courseid) { $cm = $modinfo->get_cm($taggeditem->cmid); $builder->set_accessible($taggeditem, $cm->uservisible); } }); } $items = $builder->get_items(); if (count($items) > $perpage) { $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists. array_pop($items); } // Build the display contents. if ($items) { $tagfeed = new core_tag\output\tagfeed(); foreach ($items as $item) { context_helper::preload_from_record($item); $course = $builder->get_course($item->courseid); $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($item->cmid); $courseurl = course_get_url($item->courseid, $cm->sectionnum); $cmname = $cm->get_formatted_name(); if (!$exclusivemode) { $cmname = shorten_text($cmname, 100); } $cmname = html_writer::link($cm->url?:$courseurl, $cmname); $coursename = format_string($course->fullname, true, array('context' => context_course::instance($item->courseid))); $coursename = html_writer::link($courseurl, $coursename); $icon = html_writer::empty_tag('img', array('src' => $cm->get_icon_url())); $tagfeed->add($icon, $cmname, $coursename); } $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT)); return new core_tag\output\tagindex($tag, 'core', 'course_modules', $content, $exclusivemode, $fromcontextid, $contextid, $recursivecontext, $page, $totalpages); } } /** * Return an object with the list of navigation options in a course that are avaialable or not for the current user. * This function also handles the frontpage course. * * @param stdClass $context context object (it can be a course context or the system context for frontpage settings) * @param stdClass $course the course where the settings are being rendered * @return stdClass the navigation options in a course and their availability status * @since Moodle 3.2 */ function course_get_user_navigation_options($context, $course = null) { global $CFG, $USER; $isloggedin = isloggedin(); $isguestuser = isguestuser(); $isfrontpage = $context->contextlevel == CONTEXT_SYSTEM; if ($isfrontpage) { $sitecontext = $context; } else { $sitecontext = context_system::instance(); } // Sets defaults for all options. $options = (object) [ 'badges' => false, 'blogs' => false, 'competencies' => false, 'grades' => false, 'notes' => false, 'participants' => false, 'search' => false, 'tags' => false, 'communication' => false, 'overview' => false, ]; $options->blogs = !empty($CFG->enableblogs) && ($CFG->bloglevel == BLOG_GLOBAL_LEVEL || ($CFG->bloglevel == BLOG_SITE_LEVEL and ($isloggedin and !$isguestuser))) && has_capability('moodle/blog:view', $sitecontext); $options->notes = !empty($CFG->enablenotes) && has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $context); // Frontpage settings? if ($isfrontpage) { // We are on the front page, so make sure we use the proper capability (site:viewparticipants). $options->participants = course_can_view_participants($sitecontext); $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext); $options->tags = !empty($CFG->usetags) && $isloggedin; $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext); } else { // We are in a course, so make sure we use the proper capability (course:viewparticipants). $options->participants = course_can_view_participants($context); // Only display badges if they are enabled and the current user can manage them or if they can view them and have, // at least, one available badge. if (!empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges)) { $canmanage = has_any_capability([ 'moodle/badges:createbadge', 'moodle/badges:awardbadge', 'moodle/badges:configurecriteria', 'moodle/badges:configuremessages', 'moodle/badges:configuredetails', 'moodle/badges:deletebadge', ], $context ); $totalbadges = []; $canview = false; if (!$canmanage) { // This only needs to be calculated if the user can't manage badges (to improve performance). $canview = has_capability('moodle/badges:viewbadges', $context); if ($canview) { require_once($CFG->dirroot.'/lib/badgeslib.php'); if (is_null($course)) { $totalbadges = count(badges_get_badges(BADGE_TYPE_SITE, 0, '', '', 0, 0, $USER->id)); } else { $totalbadges = count(badges_get_badges(BADGE_TYPE_COURSE, $course->id, '', '', 0, 0, $USER->id)); } } } $options->badges = ($canmanage || ($canview && $totalbadges > 0)); } // Add view grade report is permitted. $grades = false; if (has_capability('moodle/grade:viewall', $context)) { $grades = true; } else if (!empty($course->showgrades)) { $reports = core_component::get_plugin_list('gradereport'); if (is_array($reports) && count($reports) > 0) { // Get all installed reports. arsort($reports); // User is last, we want to test it first. foreach ($reports as $plugin => $plugindir) { if (has_capability('gradereport/'.$plugin.':view', $context)) { // Stop when the first visible plugin is found. $grades = true; break; } } } } $options->grades = $grades; } if (\core_communication\api::is_available()) { $options->communication = has_capability('moodle/course:configurecoursecommunication', $context); } if (\core_competency\api::is_enabled()) { $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); $options->competencies = has_any_capability($capabilities, $context); } if ($isloggedin && !$isfrontpage) { $options->overview = has_capability('moodle/course:viewoverview', $context); } return $options; } /** * Return an object with the list of administration options in a course that are available or not for the current user. * This function also handles the frontpage settings. * * @param stdClass $course course object (for frontpage it should be a clone of $SITE) * @param context_course $context context object (course context) * @return stdClass the administration options in a course and their availability status * @since Moodle 3.2 */ function course_get_user_administration_options($course, $context) { global $CFG; $isfrontpage = $course->id == SITEID; $hascompletionoptions = count(core_completion\manager::get_available_completion_options($course->id)) > 0; $options = new stdClass; $options->update = has_capability('moodle/course:update', $context); $options->editcompletion = $CFG->enablecompletion && $course->enablecompletion && ($options->update || $hascompletionoptions); $options->filters = has_capability('moodle/filter:manage', $context) && count(filter_get_available_in_context($context)) > 0; $options->reports = has_capability('moodle/site:viewreports', $context); $options->backup = has_capability('moodle/backup:backupcourse', $context); $options->restore = has_capability('moodle/restore:restorecourse', $context); $options->copy = \core_course\management\helper::can_copy_course($course->id); $options->files = ($course->legacyfiles == 2 && has_capability('moodle/course:managefiles', $context)); if (!$isfrontpage) { $options->tags = core_tag_tag::is_enabled('core', 'course') && has_capability('moodle/course:tag', $context); $options->gradebook = has_capability('moodle/grade:manage', $context); $options->outcomes = !empty($CFG->enableoutcomes) && has_capability('moodle/course:update', $context); $options->badges = !empty($CFG->enablebadges); $options->import = has_capability('moodle/restore:restoretargetimport', $context); $options->reset = has_capability('moodle/course:reset', $context); $options->roles = has_capability('moodle/role:switchroles', $context); } else { // Set default options to false. $listofoptions = array('tags', 'gradebook', 'outcomes', 'badges', 'import', 'publish', 'reset', 'roles', 'grades'); foreach ($listofoptions as $option) { $options->$option = false; } } return $options; } /** * Validates course start and end dates. * * Checks that the end course date is not greater than the start course date. * * $coursedata['startdate'] or $coursedata['enddate'] may not be set, it depends on the form and user input. * * @param array $coursedata May contain startdate and enddate timestamps, depends on the user input. * @return mixed False if everything alright, error codes otherwise. */ function course_validate_dates($coursedata) { // If both start and end dates are set end date should be later than the start date. if (!empty($coursedata['startdate']) && !empty($coursedata['enddate']) && ($coursedata['enddate'] < $coursedata['startdate'])) { return 'enddatebeforestartdate'; } // If start date is not set end date can not be set. if (empty($coursedata['startdate']) && !empty($coursedata['enddate'])) { return 'nostartdatenoenddate'; } return false; } /** * Check for course updates in the given context level instances (only modules supported right Now) * * @param stdClass $course course object * @param array $tocheck instances to check for updates * @param array $filter check only for updates in these areas * @return array list of warnings and instances with updates information * @since Moodle 3.2 */ function course_check_updates($course, $tocheck, $filter = array()) { global $CFG, $DB; $instances = array(); $warnings = array(); $modulescallbacksupport = array(); $modinfo = get_fast_modinfo($course); $supportedplugins = get_plugin_list_with_function('mod', 'check_updates_since'); // Check instances. foreach ($tocheck as $instance) { if ($instance['contextlevel'] == 'module') { // Check module visibility. try { $cm = $modinfo->get_cm($instance['id']); } catch (Exception $e) { $warnings[] = array( 'item' => 'module', 'itemid' => $instance['id'], 'warningcode' => 'cmidnotincourse', 'message' => 'This module id does not belong to this course.' ); continue; } if (!$cm->uservisible) { $warnings[] = array( 'item' => 'module', 'itemid' => $instance['id'], 'warningcode' => 'nonuservisible', 'message' => 'You don\'t have access to this module.' ); continue; } if (empty($supportedplugins['mod_' . $cm->modname])) { $warnings[] = array( 'item' => 'module', 'itemid' => $instance['id'], 'warningcode' => 'missingcallback', 'message' => 'This module does not implement the check_updates_since callback: ' . $instance['contextlevel'], ); continue; } // Retrieve the module instance. $instances[] = array( 'contextlevel' => $instance['contextlevel'], 'id' => $instance['id'], 'updates' => call_user_func($cm->modname . '_check_updates_since', $cm, $instance['since'], $filter) ); } else { $warnings[] = array( 'item' => 'contextlevel', 'itemid' => $instance['id'], 'warningcode' => 'contextlevelnotsupported', 'message' => 'Context level not yet supported ' . $instance['contextlevel'], ); } } return array($instances, $warnings); } /** * This function classifies a course as past, in progress or future. * * This function may incur a DB hit to calculate course completion. * @param stdClass $course Course record * @param stdClass $user User record (optional - defaults to $USER). * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required). * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST) */ function course_classify_for_timeline($course, $user = null, $completioninfo = null) { global $USER; if ($user == null) { $user = $USER; } if ($completioninfo == null) { $completioninfo = new completion_info($course); } // Let plugins override data for timeline classification. $pluginsfunction = get_plugins_with_function('extend_course_classify_for_timeline', 'lib.php'); foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { $pluginfunction($course, $user, $completioninfo); } } $today = time(); // End date past. if (!empty($course->enddate) && (course_classify_end_date($course) < $today)) { return COURSE_TIMELINE_PAST; } // Course was completed. if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) { return COURSE_TIMELINE_PAST; } // Start date not reached. if (!empty($course->startdate) && (course_classify_start_date($course) > $today)) { return COURSE_TIMELINE_FUTURE; } // Everything else is in progress. return COURSE_TIMELINE_INPROGRESS; } /** * This function calculates the end date to use for display classification purposes, * incorporating the grace period, if any. * * @param stdClass $course The course record. * @return int The new enddate. */ function course_classify_end_date($course) { global $CFG; $coursegraceperiodafter = (empty($CFG->coursegraceperiodafter)) ? 0 : $CFG->coursegraceperiodafter; $enddate = (new \DateTimeImmutable())->setTimestamp($course->enddate)->modify("+{$coursegraceperiodafter} days"); return $enddate->getTimestamp(); } /** * This function calculates the start date to use for display classification purposes, * incorporating the grace period, if any. * * @param stdClass $course The course record. * @return int The new startdate. */ function course_classify_start_date($course) { global $CFG; $coursegraceperiodbefore = (empty($CFG->coursegraceperiodbefore)) ? 0 : $CFG->coursegraceperiodbefore; $startdate = (new \DateTimeImmutable())->setTimestamp($course->startdate)->modify("-{$coursegraceperiodbefore} days"); return $startdate->getTimestamp(); } /** * Group a list of courses into either past, future, or in progress. * * The return value will be an array indexed by the COURSE_TIMELINE_* constants * with each value being an array of courses in that group. * E.g. * [ * COURSE_TIMELINE_PAST => [... list of past courses ...], * COURSE_TIMELINE_FUTURE => [], * COURSE_TIMELINE_INPROGRESS => [] * ] * * @param array $courses List of courses to be grouped. * @return array */ function course_classify_courses_for_timeline(array $courses) { return array_reduce($courses, function($carry, $course) { $classification = course_classify_for_timeline($course); array_push($carry[$classification], $course); return $carry; }, [ COURSE_TIMELINE_PAST => [], COURSE_TIMELINE_FUTURE => [], COURSE_TIMELINE_INPROGRESS => [] ]); } /** * Get the list of enrolled courses for the current user. * * This function returns a Generator. The courses will be loaded from the database * in chunks rather than a single query. * * @param int $limit Restrict result set to this amount * @param int $offset Skip this number of records from the start of the result set * @param string|null $sort SQL string for sorting * @param string|null $fields SQL string for fields to be returned * @param int $dbquerylimit The number of records to load per DB request * @param array $includecourses courses ids to be restricted * @param array $hiddencourses courses ids to be excluded * @return Generator */ function course_get_enrolled_courses_for_logged_in_user( int $limit = 0, int $offset = 0, ?string $sort = null, ?string $fields = null, int $dbquerylimit = COURSE_DB_QUERY_LIMIT, array $includecourses = [], array $hiddencourses = [] ): Generator { $haslimit = !empty($limit); $recordsloaded = 0; $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit; while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $includecourses, false, $offset, $hiddencourses)) { yield from $courses; $recordsloaded += $querylimit; if (count($courses) < $querylimit) { break; } if ($haslimit && $recordsloaded >= $limit) { break; } $offset += $querylimit; } } /** * Get the list of enrolled courses the current user searched for. * * This function returns a Generator. The courses will be loaded from the database * in chunks rather than a single query. * * @param int $limit Restrict result set to this amount * @param int $offset Skip this number of records from the start of the result set * @param string|null $sort SQL string for sorting * @param string|null $fields SQL string for fields to be returned * @param int $dbquerylimit The number of records to load per DB request * @param array $searchcriteria contains search criteria * @param array $options display options, same as in get_courses() except 'recursive' is ignored - * search is always category-independent * @return Generator */ function course_get_enrolled_courses_for_logged_in_user_from_search( int $limit = 0, int $offset = 0, ?string $sort = null, ?string $fields = null, int $dbquerylimit = COURSE_DB_QUERY_LIMIT, array $searchcriteria = [], array $options = [] ): Generator { $haslimit = !empty($limit); $recordsloaded = 0; $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit; $ids = core_course_category::search_courses($searchcriteria, $options); // If no courses were found matching the criteria return back. if (empty($ids)) { return; } while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $ids, false, $offset)) { yield from $courses; $recordsloaded += $querylimit; if (count($courses) < $querylimit) { break; } if ($haslimit && $recordsloaded >= $limit) { break; } $offset += $querylimit; } } /** * Search the given $courses for any that match the given $classification up to the specified * $limit. * * This function will return the subset of courses that match the classification as well as the * number of courses it had to process to build that subset. * * It is recommended that for larger sets of courses this function is given a Generator that loads * the courses from the database in chunks. * * @param array|Traversable $courses List of courses to process * @param string $classification One of the COURSE_TIMELINE_* constants * @param int $limit Limit the number of results to this amount * @return array First value is the filtered courses, second value is the number of courses processed */ function course_filter_courses_by_timeline_classification( $courses, string $classification, int $limit = 0 ): array { if (!in_array($classification, [COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_HIDDEN, COURSE_TIMELINE_SEARCH])) { $message = 'Classification must be one of COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, ' . 'COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_SEARCH or COURSE_TIMELINE_FUTURE'; throw new moodle_exception($message); } $filteredcourses = []; $numberofcoursesprocessed = 0; $filtermatches = 0; foreach ($courses as $course) { $numberofcoursesprocessed++; $pref = get_user_preferences('block_myoverview_hidden_course_' . $course->id, 0); // Added as of MDL-63457 toggle viewability for each user. if ($classification == COURSE_TIMELINE_ALLINCLUDINGHIDDEN || ($classification == COURSE_TIMELINE_HIDDEN && $pref) || $classification == COURSE_TIMELINE_SEARCH|| (($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) && !$pref)) { $filteredcourses[] = $course; $filtermatches++; } if ($limit && $filtermatches >= $limit) { // We've found the number of requested courses. No need to continue searching. break; } } // Return the number of filtered courses as well as the number of courses that were searched // in order to find the matching courses. This allows the calling code to do some kind of // pagination. return [$filteredcourses, $numberofcoursesprocessed]; } /** * Search the given $courses for any that match the given $classification up to the specified * $limit. * * This function will return the subset of courses that are favourites as well as the * number of courses it had to process to build that subset. * * It is recommended that for larger sets of courses this function is given a Generator that loads * the courses from the database in chunks. * * @param array|Traversable $courses List of courses to process * @param array $favouritecourseids Array of favourite courses. * @param int $limit Limit the number of results to this amount * @return array First value is the filtered courses, second value is the number of courses processed */ function course_filter_courses_by_favourites( $courses, $favouritecourseids, int $limit = 0 ): array { $filteredcourses = []; $numberofcoursesprocessed = 0; $filtermatches = 0; foreach ($courses as $course) { $numberofcoursesprocessed++; if (in_array($course->id, $favouritecourseids)) { $filteredcourses[] = $course; $filtermatches++; } if ($limit && $filtermatches >= $limit) { // We've found the number of requested courses. No need to continue searching. break; } } // Return the number of filtered courses as well as the number of courses that were searched // in order to find the matching courses. This allows the calling code to do some kind of // pagination. return [$filteredcourses, $numberofcoursesprocessed]; } /** * Search the given $courses for any that have a $customfieldname value that matches the given * $customfieldvalue, up to the specified $limit. * * This function will return the subset of courses that matches the value as well as the * number of courses it had to process to build that subset. * * It is recommended that for larger sets of courses this function is given a Generator that loads * the courses from the database in chunks. * * @param array|Traversable $courses List of courses to process * @param string $customfieldname the shortname of the custom field to match against * @param string $customfieldvalue the value this custom field needs to match * @param int $limit Limit the number of results to this amount * @return array First value is the filtered courses, second value is the number of courses processed */ function course_filter_courses_by_customfield( $courses, $customfieldname, $customfieldvalue, int $limit = 0 ): array { global $DB; if (!$courses) { return [[], 0]; } // Prepare the list of courses to search through. $coursesbyid = []; foreach ($courses as $course) { $coursesbyid[$course->id] = $course; } if (!$coursesbyid) { return [[], 0]; } list($csql, $params) = $DB->get_in_or_equal(array_keys($coursesbyid), SQL_PARAMS_NAMED); // Get the id of the custom field. $sql = " SELECT f.id FROM {customfield_field} f JOIN {customfield_category} cat ON cat.id = f.categoryid WHERE f.shortname = ? AND cat.component = 'core_course' AND cat.area = 'course' "; $fieldid = $DB->get_field_sql($sql, [$customfieldname]); if (!$fieldid) { return [[], 0]; } // Get a list of courseids that match that custom field value. if ($customfieldvalue == COURSE_CUSTOMFIELD_EMPTY) { $comparevalue = $DB->sql_compare_text('cd.value'); $sql = " SELECT c.id FROM {course} c LEFT JOIN {customfield_data} cd ON cd.instanceid = c.id AND cd.fieldid = :fieldid WHERE c.id $csql AND (cd.value IS NULL OR $comparevalue = '' OR $comparevalue = '0') "; $params['fieldid'] = $fieldid; $matchcourseids = $DB->get_fieldset_sql($sql, $params); } else { $comparevalue = $DB->sql_compare_text('value'); $select = "fieldid = :fieldid AND $comparevalue = :customfieldvalue AND instanceid $csql"; $params['fieldid'] = $fieldid; $params['customfieldvalue'] = $customfieldvalue; $matchcourseids = $DB->get_fieldset_select('customfield_data', 'instanceid', $select, $params); } // Prepare the list of courses to return. $filteredcourses = []; $numberofcoursesprocessed = 0; $filtermatches = 0; foreach ($coursesbyid as $course) { $numberofcoursesprocessed++; if (in_array($course->id, $matchcourseids)) { $filteredcourses[] = $course; $filtermatches++; } if ($limit && $filtermatches >= $limit) { // We've found the number of requested courses. No need to continue searching. break; } } // Return the number of filtered courses as well as the number of courses that were searched // in order to find the matching courses. This allows the calling code to do some kind of // pagination. return [$filteredcourses, $numberofcoursesprocessed]; } /** * Check module updates since a given time. * This function checks for updates in the module config, file areas, completion, grades, comments and ratings. * * @param cm_info $cm course module data * @param int $from the time to check * @param array $fileareas additional file ares to check * @param array $filter if we need to filter and return only selected updates * @return stdClass object with the different updates * @since Moodle 3.2 */ function course_check_module_updates_since($cm, $from, $fileareas = array(), $filter = array()) { global $DB, $CFG, $USER; $context = $cm->context; $mod = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST); $updates = new stdClass(); $course = get_course($cm->course); $component = 'mod_' . $cm->modname; // Check changes in the module configuration. if (isset($mod->timemodified) and (empty($filter) or in_array('configuration', $filter))) { $updates->configuration = (object) array('updated' => false); if ($updates->configuration->updated = $mod->timemodified > $from) { $updates->configuration->timeupdated = $mod->timemodified; } } // Check for updates in files. if (plugin_supports('mod', $cm->modname, FEATURE_MOD_INTRO)) { $fileareas[] = 'intro'; } if (!empty($fileareas) and (empty($filter) or in_array('fileareas', $filter))) { $fs = get_file_storage(); $files = $fs->get_area_files($context->id, $component, $fileareas, false, "filearea, timemodified DESC", false, $from); foreach ($fileareas as $filearea) { $updates->{$filearea . 'files'} = (object) array('updated' => false); } foreach ($files as $file) { $updates->{$file->get_filearea() . 'files'}->updated = true; $updates->{$file->get_filearea() . 'files'}->itemids[] = $file->get_id(); } } // Check completion. $supportcompletion = plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_HAS_RULES); $supportcompletion = $supportcompletion or plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_TRACKS_VIEWS); if ($supportcompletion and (empty($filter) or in_array('completion', $filter))) { $updates->completion = (object) array('updated' => false); $completion = new completion_info($course); // Use wholecourse to cache all the modules the first time. $completiondata = $completion->get_data($cm, true); if ($updates->completion->updated = !empty($completiondata->timemodified) && $completiondata->timemodified > $from) { $updates->completion->timemodified = $completiondata->timemodified; } } // Check grades. $supportgrades = plugin_supports('mod', $cm->modname, FEATURE_GRADE_HAS_GRADE); $supportgrades = $supportgrades or plugin_supports('mod', $cm->modname, FEATURE_GRADE_OUTCOMES); if ($supportgrades and (empty($filter) or (in_array('gradeitems', $filter) or in_array('outcomes', $filter)))) { require_once($CFG->libdir . '/gradelib.php'); $grades = grade_get_grades($course->id, 'mod', $cm->modname, $mod->id, $USER->id); if (empty($filter) or in_array('gradeitems', $filter)) { $updates->gradeitems = (object) array('updated' => false); foreach ($grades->items as $gradeitem) { foreach ($gradeitem->grades as $grade) { if ($grade->datesubmitted > $from or $grade->dategraded > $from) { $updates->gradeitems->updated = true; $updates->gradeitems->itemids[] = $gradeitem->id; } } } } if (empty($filter) or in_array('outcomes', $filter)) { $updates->outcomes = (object) array('updated' => false); foreach ($grades->outcomes as $outcome) { foreach ($outcome->grades as $grade) { if ($grade->datesubmitted > $from or $grade->dategraded > $from) { $updates->outcomes->updated = true; $updates->outcomes->itemids[] = $outcome->id; } } } } } // Check comments. if (plugin_supports('mod', $cm->modname, FEATURE_COMMENT) and (empty($filter) or in_array('comments', $filter))) { $updates->comments = (object) array('updated' => false); require_once($CFG->dirroot . '/comment/lib.php'); require_once($CFG->dirroot . '/comment/locallib.php'); $manager = new comment_manager(); $comments = $manager->get_component_comments_since($course, $context, $component, $from, $cm); if (!empty($comments)) { $updates->comments->updated = true; $updates->comments->itemids = array_keys($comments); } } // Check ratings. if (plugin_supports('mod', $cm->modname, FEATURE_RATE) and (empty($filter) or in_array('ratings', $filter))) { $updates->ratings = (object) array('updated' => false); require_once($CFG->dirroot . '/rating/lib.php'); $manager = new rating_manager(); $ratings = $manager->get_component_ratings_since($context, $component, $from); if (!empty($ratings)) { $updates->ratings->updated = true; $updates->ratings->itemids = array_keys($ratings); } } return $updates; } /** * Returns true if the user can view the participant page, false otherwise, * * @param context $context The context we are checking. * @return bool */ function course_can_view_participants($context) { $viewparticipantscap = 'moodle/course:viewparticipants'; if ($context->contextlevel == CONTEXT_SYSTEM) { $viewparticipantscap = 'moodle/site:viewparticipants'; } return has_any_capability([$viewparticipantscap, 'moodle/course:enrolreview'], $context); } /** * Checks if a user can view the participant page, if not throws an exception. * * @param context $context The context we are checking. * @throws required_capability_exception */ function course_require_view_participants($context) { if (!course_can_view_participants($context)) { $viewparticipantscap = 'moodle/course:viewparticipants'; if ($context->contextlevel == CONTEXT_SYSTEM) { $viewparticipantscap = 'moodle/site:viewparticipants'; } throw new required_capability_exception($context, $viewparticipantscap, 'nopermissions', ''); } } /** * Return whether the user can download from the specified backup file area in the given context. * * @param string $filearea the backup file area. E.g. 'course', 'backup' or 'automated'. * @param \context $context * @param stdClass $user the user object. If not provided, the current user will be checked. * @return bool true if the user is allowed to download in the context, false otherwise. */ function can_download_from_backup_filearea($filearea, \context $context, ?stdClass $user = null) { $candownload = false; switch ($filearea) { case 'course': case 'backup': $candownload = has_capability('moodle/backup:downloadfile', $context, $user); break; case 'automated': // Given the automated backups may contain userinfo, we restrict access such that only users who are able to // restore with userinfo are able to download the file. Users can't create these backups, so checking 'backup:userinfo' // doesn't make sense here. $candownload = has_capability('moodle/backup:downloadfile', $context, $user) && has_capability('moodle/restore:userinfo', $context, $user); break; default: break; } return $candownload; } /** * Get a list of hidden courses * * @param int|object|null $user User override to get the filter from. Defaults to current user * @return array $ids List of hidden courses * @throws coding_exception */ function get_hidden_courses_on_timeline($user = null) { global $USER; if (empty($user)) { $user = $USER->id; } $preferences = get_user_preferences(null, null, $user); $ids = []; foreach ($preferences as $key => $value) { if (preg_match('/block_myoverview_hidden_course_(\d)+/', $key)) { $id = preg_split('/block_myoverview_hidden_course_/', $key); $ids[] = $id[1]; } } return $ids; } /** * Returns a list of the most recently courses accessed by a user * * @param int $userid User id from which the courses will be obtained * @param int $limit Restrict result set to this amount * @param int $offset Skip this number of records from the start of the result set * @param string|null $sort SQL string for sorting * @return array */ function course_get_recent_courses(?int $userid = null, int $limit = 0, int $offset = 0, ?string $sort = null) { global $CFG, $USER, $DB; if (empty($userid)) { $userid = $USER->id; } $basefields = [ 'id', 'idnumber', 'summary', 'summaryformat', 'startdate', 'enddate', 'category', 'shortname', 'fullname', 'timeaccess', 'component', 'visible', 'showactivitydates', 'showcompletionconditions', 'pdfexportfont' ]; if (empty($sort)) { $sort = 'timeaccess DESC'; } else { // The SQL string for sorting can define sorting by multiple columns. $rawsorts = explode(',', $sort); $sorts = array(); // Validate and trim the sort parameters in the SQL string for sorting. foreach ($rawsorts as $rawsort) { $sort = trim($rawsort); $sortparams = explode(' ', $sort); // A valid sort statement can not have more than 2 params (ex. 'summary desc' or 'timeaccess'). if (count($sortparams) > 2) { throw new invalid_parameter_exception( 'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].'); } $sortfield = trim($sortparams[0]); // Validate the value which defines the field to sort by. if (!in_array($sortfield, $basefields)) { throw new invalid_parameter_exception('Invalid field in the sort parameter, allowed fields: ' . implode(', ', $basefields) . '.'); } $sortdirection = isset($sortparams[1]) ? trim($sortparams[1]) : ''; // Validate the value which defines the sort direction (if present). $allowedsortdirections = ['asc', 'desc']; if (!empty($sortdirection) && !in_array(strtolower($sortdirection), $allowedsortdirections)) { throw new invalid_parameter_exception('Invalid sort direction in the sort parameter, allowed values: ' . implode(', ', $allowedsortdirections) . '.'); } $sorts[] = $sort; } $sort = implode(',', $sorts); } $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); $coursefields = 'c.' . join(',', $basefields); // Ask the favourites service to give us the join SQL for favourited courses, // so we can include favourite information in the query. $usercontext = \context_user::instance($userid); $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_course', 'courses', 'fav', 'ul.courseid'); $sql = "SELECT $coursefields, $ctxfields FROM {course} c JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id JOIN {user_lastaccess} ul ON ul.courseid = c.id $favsql LEFT JOIN {enrol} eg ON eg.courseid = c.id AND eg.status = :statusenrolg AND eg.enrol = :guestenrol WHERE ul.userid = :userid AND c.visible = :visible AND (eg.id IS NOT NULL OR EXISTS (SELECT e.id FROM {enrol} e JOIN {user_enrolments} ue ON ue.enrolid = e.id WHERE e.courseid = c.id AND e.status = :statusenrol AND ue.status = :status AND ue.userid = :userid2 AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2) )) ORDER BY $sort"; $now = round(time(), -2); // Improves db caching. $params = ['userid' => $userid, 'contextlevel' => CONTEXT_COURSE, 'visible' => 1, 'status' => ENROL_USER_ACTIVE, 'statusenrol' => ENROL_INSTANCE_ENABLED, 'guestenrol' => 'guest', 'now1' => $now, 'now2' => $now, 'userid2' => $userid, 'statusenrolg' => ENROL_INSTANCE_ENABLED] + $favparams; $recentcourses = $DB->get_records_sql($sql, $params, $offset, $limit); // Filter courses if last access field is hidden. $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); if ($userid != $USER->id && isset($hiddenfields['lastaccess'])) { $recentcourses = array_filter($recentcourses, function($course) { context_helper::preload_from_record($course); $context = context_course::instance($course->id, IGNORE_MISSING); // If last access was a hidden field, a user requesting info about another user would need permission to view hidden // fields. return has_capability('moodle/course:viewhiddenuserfields', $context); }); } return $recentcourses; } /** * Calculate the course start date and offset for the given user ids. * * If the course is a fixed date course then the course start date will be returned. * If the course is a relative date course then the course date will be calculated and * and offset provided. * * The dates are returned as an array with the index being the user id. The array * contains the start date and start offset values for the user. * * If the user is not enrolled in the course then the course start date will be returned. * * If we have a course which starts on 1563244000 and 2 users, id 123 and 456, where the * former is enrolled in the course at 1563244693 and the latter is not enrolled then the * return value would look like: * [ * '123' => [ * 'start' => 1563244693, * 'startoffset' => 693 * ], * '456' => [ * 'start' => 1563244000, * 'startoffset' => 0 * ] * ] * * @param stdClass $course The course to fetch dates for. * @param array $userids The list of user ids to get dates for. * @return array */ function course_get_course_dates_for_user_ids(stdClass $course, array $userids): array { if (empty($course->relativedatesmode)) { // This course isn't set to relative dates so we can early return with the course // start date. return array_reduce($userids, function($carry, $userid) use ($course) { $carry[$userid] = [ 'start' => $course->startdate, 'startoffset' => 0 ]; return $carry; }, []); } // We're dealing with a relative dates course now so we need to calculate some dates. $cache = cache::make('core', 'course_user_dates'); $dates = []; $uncacheduserids = []; // Try fetching the values from the cache so that we don't need to do a DB request. foreach ($userids as $userid) { $cachekey = "{$course->id}_{$userid}"; $cachedvalue = $cache->get($cachekey); if ($cachedvalue === false) { // Looks like we haven't seen this user for this course before so we'll have // to fetch it. $uncacheduserids[] = $userid; } else { [$start, $startoffset] = $cachedvalue; $dates[$userid] = [ 'start' => $start, 'startoffset' => $startoffset ]; } } if (!empty($uncacheduserids)) { // Load the enrolments for any users we haven't seen yet. Set the "onlyactive" param // to false because it filters out users with enrolment start times in the future which // we don't want. $enrolments = enrol_get_course_users($course->id, false, $uncacheduserids); foreach ($uncacheduserids as $userid) { // Find the user enrolment that has the earliest start date. $enrolment = array_reduce(array_values($enrolments), function($carry, $enrolment) use ($userid) { // Only consider enrolments for this user if the user enrolment is active and the // enrolment method is enabled. if ( $enrolment->uestatus == ENROL_USER_ACTIVE && $enrolment->estatus == ENROL_INSTANCE_ENABLED && $enrolment->id == $userid ) { if (is_null($carry)) { // Haven't found an enrolment yet for this user so use the one we just found. $carry = $enrolment; } else { // We've already found an enrolment for this user so let's use which ever one // has the earliest start time. $carry = $carry->uetimestart < $enrolment->uetimestart ? $carry : $enrolment; } } return $carry; }, null); if ($enrolment) { // The course is in relative dates mode so we calculate the student's start // date based on their enrolment start date. $start = $course->startdate > $enrolment->uetimestart ? $course->startdate : $enrolment->uetimestart; $startoffset = $start - $course->startdate; } else { // The user is not enrolled in the course so default back to the course start date. $start = $course->startdate; $startoffset = 0; } $dates[$userid] = [ 'start' => $start, 'startoffset' => $startoffset ]; $cachekey = "{$course->id}_{$userid}"; $cache->set($cachekey, [$start, $startoffset]); } } return $dates; } /** * Calculate the course start date and offset for the given user id. * * If the course is a fixed date course then the course start date will be returned. * If the course is a relative date course then the course date will be calculated and * and offset provided. * * The return array contains the start date and start offset values for the user. * * If the user is not enrolled in the course then the course start date will be returned. * * If we have a course which starts on 1563244000. If a user's enrolment starts on 1563244693 * then the return would be: * [ * 'start' => 1563244693, * 'startoffset' => 693 * ] * * If the use was not enrolled then the return would be: * [ * 'start' => 1563244000, * 'startoffset' => 0 * ] * * @param stdClass $course The course to fetch dates for. * @param int $userid The user id to get dates for. * @return array */ function course_get_course_dates_for_user_id(stdClass $course, int $userid): array { return (course_get_course_dates_for_user_ids($course, [$userid]))[$userid]; } /** * Renders the course copy form for the modal on the course management screen. * * @param array $args * @return string $o Form HTML. */ function course_output_fragment_new_base_form($args) { $serialiseddata = json_decode($args['jsonformdata'], true); $formdata = []; if (!empty($serialiseddata)) { parse_str($serialiseddata, $formdata); } $context = context_course::instance($args['courseid']); $copycaps = \core_course\management\helper::get_course_copy_capabilities(); require_all_capabilities($copycaps, $context); $course = get_course($args['courseid']); $mform = new \core_backup\output\copy_form( null, array('course' => $course, 'returnto' => '', 'returnurl' => ''), 'post', '', ['class' => 'ignoredirty'], true, $formdata); if (!empty($serialiseddata)) { // If we were passed non-empty form data we want the mform to call validation functions and show errors. $mform->is_validated(); } ob_start(); $mform->display(); $o = ob_get_contents(); ob_end_clean(); return $o; } /** * Get the course overview fragment. * * @param array $args the fragment arguments * @return string the course overview fragment * * @throws require_login_exception */ function course_output_fragment_course_overview($args) { global $PAGE; if (empty($args['modname']) || empty($args['courseid'])) { throw new coding_exception('modname and courseid are required'); } $modname = $args['modname']; $course = get_course($args['courseid']); $context = context_course::instance($course->id, MUST_EXIST); if (!can_access_course($course, null, '', true)) { throw new require_login_exception('Course is not available'); } // Some plugins may have a list view event. $eventclassname = 'mod_' . $modname . '\\event\\course_module_instance_list_viewed'; // Do not confuse this "resource" with the "mod_resource" module. // This "resource" is the table that aggregate all activities considered "resources" // (files, folders, pages, text and media...). While the "mod_resource" is a poorly // named plugin representing an uploaded file, and it is also one of the activities // that can be aggregated in the "resource" table. if ($modname === 'resource') { $eventclassname = 'core\\event\\course_resources_list_viewed'; } if (class_exists($eventclassname)) { try { $event = $eventclassname::create(['context' => $context]); $event->add_record_snapshot('course', $course); $event->trigger(); } catch (\Throwable $th) { // This may happens if the plugin implements a custom event class. // It is highly unlikely but we should not stop the rendering because of this. // Instead, we will log the error and continue. debugging('Error while triggering the course module instance viewed event: ' . $th->getMessage()); } } $content = ''; $format = course_get_format($course); $renderer = $format->get_renderer($PAGE); // Plugins with not implemented overview table will have an extra link to the index.php. $overvietableclass = $format->get_output_classname('overview\missingoverviewnotice'); /** @var \core_courseformat\output\local\overview\missingoverviewnotice $output */ $output = new $overvietableclass($course, $modname); $content .= $renderer->render($output); $overvietableclass = $format->get_output_classname('overview\\overviewtable'); /** @var \core_courseformat\output\local\overview\overviewtable $output */ $output = new $overvietableclass($course, $modname); $content .= $renderer->render($output); return $content; } /** * Get the current course image for the given course. * * @param \stdClass $course * @return null|stored_file */ function course_get_courseimage(\stdClass $course): ?stored_file { $courseinlist = new core_course_list_element($course); foreach ($courseinlist->get_course_overviewfiles() as $file) { if ($file->is_valid_image()) { return $file; } } return null; } /** * Get course specific data for configuring a communication instance. * * @param integer $courseid The course id. * @return array Returns course data, context and heading. */ function course_get_communication_instance_data(int $courseid): array { // Do some checks and prepare instance specific data. $course = get_course($courseid); require_login($course); $context = context_course::instance($course->id); require_capability('moodle/course:configurecoursecommunication', $context); $heading = $course->fullname; $returnurl = new moodle_url('/course/view.php', ['id' => $courseid]); return [$course, $context, $heading, $returnurl]; } /** * Update a course using communication configuration data. * * @param stdClass $data The data to update the course with. */ function course_update_communication_instance_data(stdClass $data): void { $data->id = $data->instanceid; // For correct use in update_course. // If the room name is set to empty, then set it course name. $provider = $data->selectedcommunication ?? null; $roomnameidentifier = $provider . 'roomname'; if ($provider && empty($data->$roomnameidentifier)) { $data->$roomnameidentifier = $data->fullname ?? get_course($data->id)->fullname; } core_communication\helper::update_course_communication_instance( course: $data, changesincoursecat: false, ); } /** * Trigger course section viewed event. * * @param context_course $context course context object * @param int $sectionid section number * @since Moodle 4.4. */ function course_section_view(context_course $context, int $sectionid) { $eventdata = [ 'objectid' => $sectionid, 'context' => $context, ]; $event = \core\event\section_viewed::create($eventdata); $event->trigger(); user_accesstime_log($context->instanceid); } PK$9\// search.phpnu[. /** * Displays external information about a course * @package core_course * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../config.php"); require_once($CFG->dirroot.'/course/lib.php'); $q = optional_param('q', '', PARAM_RAW); // Global search words. $search = optional_param('search', '', PARAM_RAW); // search words $page = optional_param('page', 0, PARAM_INT); // which page to show $perpage = optional_param('perpage', '', PARAM_RAW); // how many per page, may be integer or 'all' $blocklist = optional_param('blocklist', 0, PARAM_INT); $modulelist= optional_param('modulelist', '', PARAM_PLUGIN); $tagid = optional_param('tagid', '', PARAM_INT); // searches for courses tagged with this tag id // Use global search. if ($q) { $search = $q; } // List of minimum capabilities which user need to have for editing/moving course $capabilities = array('moodle/course:create', 'moodle/category:manage'); // Populate usercatlist with list of category id's with course:create and category:manage capabilities. $usercatlist = core_course_category::make_categories_list($capabilities); $search = trim(strip_tags($search)); // trim & clean raw searched string $site = get_site(); $searchcriteria = array(); foreach (array('search', 'blocklist', 'modulelist', 'tagid') as $param) { if (!empty($$param)) { $searchcriteria[$param] = $$param; } } $urlparams = array(); if ($perpage !== 'all' && !($perpage = (int)$perpage)) { // default number of courses per page $perpage = $CFG->coursesperpage; } else { $urlparams['perpage'] = $perpage; } if (!empty($page)) { $urlparams['page'] = $page; } $PAGE->set_url('/course/search.php', $searchcriteria + $urlparams); $PAGE->set_context(context_system::instance()); $PAGE->set_pagelayout('standard'); $courserenderer = $PAGE->get_renderer('core', 'course'); if ($CFG->forcelogin) { require_login(); } $strcourses = new lang_string("courses"); $strsearch = new lang_string("search"); $strsearchresults = new lang_string("searchresults"); $strnovalidcourses = new lang_string('novalidcourses'); $courseurl = core_course_category::user_top() ? new moodle_url('/course/index.php') : null; $PAGE->navbar->add($strcourses, $courseurl); $PAGE->navbar->add($strsearch, new moodle_url('/course/search.php')); if (!empty($search)) { $PAGE->navbar->add(s($search)); } if (empty($searchcriteria)) { // no search criteria specified, print page with just search form $PAGE->set_title($strsearch); } else { // this is search results page $PAGE->set_title($strsearchresults); // Link to manage search results should be visible if user have system or category level capability if ((can_edit_in_category() || !empty($usercatlist))) { $aurl = new moodle_url('/course/management.php', $searchcriteria); $searchform = $OUTPUT->single_button($aurl, get_string('managecourses'), 'get'); $PAGE->set_button($searchform); } // Trigger event, courses searched. $eventparams = array('context' => $PAGE->context, 'other' => array('query' => $search)); $event = \core\event\courses_searched::create($eventparams); $event->trigger(); } $PAGE->set_heading($site->fullname); echo $OUTPUT->header(); echo $courserenderer->search_courses($searchcriteria); echo $OUTPUT->footer(); PK$9\deee reset.phpnu[. /** * The purpose of this feature is to quickly remove all user related data from a course * in order to make it available for a new semester. This feature can handle the removal * of general course data like students, teachers, logs, events and groups as well as module * specific data. Each module must be modified to take advantage of this new feature. * The feature will also reset the start date of the course if necessary. * * @package core_course * @copyright Mark Flach and moodle.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../config.php'); require_once('reset_form.php'); require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php'); require_once($CFG->dirroot . '/backup/backup.class.php'); require_once($CFG->dirroot . '/backup/util/helper/backup_helper.class.php'); $id = required_param('id', PARAM_INT); if (!$course = $DB->get_record('course', ['id' => $id])) { throw new \moodle_exception('invalidcourseid'); } $PAGE->set_url('/course/reset.php', ['id' => $id]); $PAGE->set_pagelayout('standard'); require_login($course); require_capability('moodle/course:reset', context_course::instance($course->id)); $strreset = get_string('reset'); $strresetcourse = get_string('resetcourse'); $strremove = get_string('remove'); $PAGE->set_title($course->fullname.': '.$strresetcourse); $PAGE->set_heading($course->fullname); $PAGE->set_secondary_active_tab('coursereuse'); $mform = new course_reset_form(); if ($mform->is_cancelled()) { redirect($CFG->wwwroot . '/course/view.php?id='.$id); } else if ($data = $mform->get_data()) { // No magic quotes. if (isset($data->selectdefault)) { $_POST = []; $mform = new course_reset_form(); $mform->load_defaults(); } else if (isset($data->deselectall)) { $_POST = []; $mform = new course_reset_form(); } else { echo $OUTPUT->header(); \backup_helper::print_coursereuse_selector('reset'); $data->reset_start_date_old = $course->startdate; $data->reset_end_date_old = $course->enddate; $status = reset_course_userdata($data); $data = []; foreach ($status as $item) { $line = []; $line[] = $item['component']; $line[] = $item['item']; if ($item['error'] === false) { $line[] = get_string('statusdone'); } else { $line[] = html_writer::div( $OUTPUT->pix_icon('i/invalid', get_string('error')) . $item['error'], ); } $data[] = $line; } $table = new html_table(); $table->head = [get_string('resetcomponent'), get_string('resettask'), get_string('resetstatus')]; $table->size = ['20%', '40%', '40%']; $table->align = ['left', 'left', 'left']; $table->width = '80%'; $table->data = $data; echo html_writer::table($table); echo $OUTPUT->continue_button('view.php?id=' . $course->id); // Back to course page. echo $OUTPUT->footer(); exit; } } else { $mform = new course_reset_form(); $mform->load_defaults(); } $PAGE->requires->js_call_amd('core_course/resetcourse', 'init'); echo $OUTPUT->header(); \backup_helper::print_coursereuse_selector('reset'); echo $OUTPUT->box(get_string('resetinfo')); echo $OUTPUT->box(get_string('resetinfoselect')); $mform->display(); echo $OUTPUT->footer(); PK$9\*۰ dndupload.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 library for enableing a drag and drop upload to courses * * @package core * @subpackage course * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ M.course_dndupload = { // YUI object. Y: null, // URL for upload requests url: M.cfg.wwwroot + '/course/dndupload.php', // maximum size of files allowed in this form maxbytes: 0, // ID of the course we are on courseid: null, // Data about the different file/data handlers that are available handlers: null, // Nasty hack to distinguish between dragenter(first entry), // dragenter+dragleave(moving between child elements) and dragleave (leaving element) entercount: 0, // Used to keep track of the section we are dragging across - to make // spotting movement between sections more reliable currentsection: null, // Used to store the pending uploads whilst the user is being asked for further input uploadqueue: null, // True if the there is currently a dialog being shown (asking for a name, or giving a // choice of file handlers) uploaddialog: false, // An array containing the last selected file handler for each file type lastselected: null, // The following are used to identify specific parts of the course page // The type of HTML element that is a course section sectiontypename: 'li', // The classes that an element must have to be identified as a course section sectionclasses: ['section', 'main'], // The ID of the main content area of the page (for adding the 'status' div) pagecontentid: 'page', // The selector identifying the list of modules within a section (note changing this may require // changes to the get_mods_element function) modslistselector: 'ul.section', /** * Initalise the drag and drop upload interface * Note: one and only one of options.filemanager and options.formcallback must be defined * * @param Y the YUI object * @param object options { * courseid: ID of the course we are on * maxbytes: maximum size of files allowed in this form * handlers: Data about the different file/data handlers that are available * } */ init: function(Y, options) { this.Y = Y; if (!this.browser_supported()) { return; // Browser does not support the required functionality } this.maxbytes = options.maxbytes; this.courseid = options.courseid; this.handlers = options.handlers; this.uploadqueue = new Array(); this.lastselected = new Array(); var sectionselector = this.sectiontypename + '.' + this.sectionclasses.join('.'); var sections = this.Y.all(sectionselector); if (sections.isEmpty()) { return; // No sections - incompatible course format or front page. } sections.each( function(el) { this.add_preview_element(el); this.init_events(el); }, this); if (options.showstatus) { this.add_status_div(); } var self = this; require([ 'core_courseformat/courseeditor', 'core_course/events' ], function( Editor, CourseEvents ) { // Any change to the course must be applied also to the course state via the courseeditor module. self.courseeditor = Editor.getCurrentCourseEditor(); // Some formats can add sections without reloading the page. document.querySelector('#' + self.pagecontentid).addEventListener( CourseEvents.sectionRefreshed, self.sectionRefreshed.bind(self) ); }); document.addEventListener('scroll', function() { sections.each(function(el) { if (el.hasClass('dndupload-dropzone')) { self.rePositionPreviewInfoElement(el); } }, this); }, true); }, /** * Setup Drag and Drop in a section. * @param {CustomEvent} event The custom event */ sectionRefreshed: function(event) { if (event.detail.newSectionElement === undefined) { return; } var element = this.Y.one(event.detail.newSectionElement); this.add_preview_element(element); this.init_events(element); }, /** * Add a div element to tell the user that drag and drop upload * is available (or to explain why it is not available) */ add_status_div: function() { var Y = this.Y, coursecontents = Y.one('#' + this.pagecontentid), div, handlefile = (this.handlers.filehandlers.length > 0), handletext = false, handlelink = false, i = 0, styletop, styletopunit; if (!coursecontents) { return; } div = Y.Node.create('
').setStyle('opacity', '0.0'); coursecontents.insert(div, 0); for (i = 0; i < this.handlers.types.length; i++) { switch (this.handlers.types[i].identifier) { case 'text': case 'text/html': handletext = true; break; case 'url': handlelink = true; break; } } $msgident = 'dndworking'; if (handlefile) { $msgident += 'file'; } if (handletext) { $msgident += 'text'; } if (handlelink) { $msgident += 'link'; } div.setContent(M.util.get_string($msgident, 'moodle')); styletop = div.getStyle('top') || '0px'; styletopunit = styletop.replace(/^\d+/, ''); styletop = parseInt(styletop.replace(/\D*$/, ''), 10); var fadein = new Y.Anim({ node: '#dndupload-status', from: { opacity: 0.0, top: (styletop - 30).toString() + styletopunit }, to: { opacity: 1.0, top: styletop.toString() + styletopunit }, duration: 0.5 }); var fadeout = new Y.Anim({ node: '#dndupload-status', from: { opacity: 1.0, top: styletop.toString() + styletopunit }, to: { opacity: 0.0, top: (styletop - 30).toString() + styletopunit }, duration: 0.5 }); fadein.run(); fadein.on('end', function(e) { Y.later(3000, this, function() { fadeout.run(); }); }); fadeout.on('end', function(e) { Y.one('#dndupload-status').remove(true); }); }, /** * Check the browser has the required functionality * @return true if browser supports drag/drop upload */ browser_supported: function() { if (typeof FileReader == 'undefined') { return false; } if (typeof FormData == 'undefined') { return false; } return true; }, /** * Initialise drag events on node container, all events need * to be processed for drag and drop to work * @param el the element to add events to */ init_events: function(el) { this.Y.on('dragenter', this.drag_enter, el, this); this.Y.on('dragleave', this.drag_leave, el, this); this.Y.on('dragover', this.drag_over, el, this); this.Y.on('drop', this.drop, el, this); }, /** * Work out which course section a given element is in * @param el the child DOM element within the section * @return the DOM element representing the section */ get_section: function(el) { var sectionclasses = this.sectionclasses; return el.ancestor( function(test) { var i; for (i=0; i 2) { this.entercount = 2; return false; } } this.showPreviewInfoElement(section); return false; }, /** * Handle a dragleave event: remove the 'add here' message (if present) * @param e event data * @return false to prevent the event from continuing to be processed */ drag_leave: function(e) { if (!this.check_drag(e)) { return false; } this.entercount--; if (this.entercount == 1) { return false; } this.entercount = 0; this.currentsection = null; this.hide_preview_element(); return false; }, /** * Handle a dragover event: just prevent the browser default (necessary * to allow drag and drop handling to work) * @param e event data * @return false to prevent the event from continuing to be processed */ drag_over: function(e) { this.check_drag(e); return false; }, /** * Handle a drop event: hide the 'add here' message, check the attached * data type and start the upload process * @param e event data * @return false to prevent the event from continuing to be processed */ drop: function(e) { this.hide_preview_element(); if (!(type = this.check_drag(e))) { return false; } // Work out the number of the section we are on (from its id) var section = this.get_section(e.currentTarget); var sectionnumber = this.get_section_number(section); // Process the file or the included data if (type.type == 'Files') { var files = e._event.dataTransfer.files; for (var i=0, f; f=files[i]; i++) { this.handle_file(f, section, sectionnumber); } } else { var contents = e._event.dataTransfer.getData(type.realtype); if (contents) { this.handle_item(type, contents, section, sectionnumber); } } return false; }, /** * Find or create the 'ul' element that contains all of the module * instances in this section * @param section the DOM element representing the section * @return false to prevent the event from continuing to be processed */ get_mods_element: function(section) { // Find the 'ul' containing the list of mods var modsel = section.one(this.modslistselector); if (!modsel) { // Create the above 'ul' if it doesn't exist modsel = document.createElement('ul'); modsel.className = 'section img-text'; var contentel = section.get('children').pop(); var brel = contentel.get('children').pop(); contentel.insertBefore(modsel, brel); modsel = this.Y.one(modsel); } return modsel; }, /** * Add a new dummy item to the list of mods, to be replaced by a real * item & link once the AJAX upload call has completed * @param name the label to show in the element * @param section the DOM element reperesenting the course section * @return DOM element containing the new item */ add_resource_element: function(name, section, module) { var modsel = this.get_mods_element(section); var resel = { parent: modsel, li: document.createElement('li'), div: document.createElement('div'), indentdiv: document.createElement('div'), a: document.createElement('a'), icon: document.createElement('img'), namespan: document.createElement('span'), groupingspan: document.createElement('span'), progressouter: document.createElement('span'), progress: document.createElement('span') }; resel.li.className = 'activity ' + module + ' modtype_' + module; resel.indentdiv.className = 'mod-indent'; resel.li.appendChild(resel.indentdiv); resel.div.className = 'activityinstance'; resel.indentdiv.appendChild(resel.div); resel.a.href = '#'; resel.div.appendChild(resel.a); resel.icon.src = M.util.image_url('i/ajaxloader'); resel.icon.className = 'activityicon'; resel.a.appendChild(resel.icon); resel.namespan.className = 'instancename'; resel.namespan.innerHTML = name; resel.a.appendChild(resel.namespan); resel.groupingspan.className = 'groupinglabel'; resel.div.appendChild(resel.groupingspan); resel.progressouter.className = 'dndupload-progress-outer'; resel.progress.className = 'dndupload-progress-inner'; resel.progress.innerHTML = ' '; resel.progressouter.appendChild(resel.progress); resel.div.appendChild(resel.progressouter); modsel.insertBefore(resel.li, modsel.get('children')); // Leave the 'preview element' at the bottom return resel; }, /** * Hide any visible dndupload-preview elements on the page */ hide_preview_element: function() { this.Y.all('.dndupload-preview-wrapper').addClass('dndupload-hidden'); this.Y.all('.dndupload-over').removeClass('dndupload-over'); this.Y.all('.dndupload-dropzone').removeClass('dndupload-dropzone'); }, /** * Unhide the preview element for the given section and set it to display * the correct message * @param section the YUI node representing the selected course section * @param type the details of the data type detected in the drag (including the message to display) * @deprecated Since Moodle 4.0. Please use showPreviewInfoElement() instead. */ show_preview_element: function(section, type) { this.hide_preview_element(); var preview = section.one('li.dndupload-preview').removeClass('dndupload-hidden'); section.addClass('dndupload-over'); // Horrible work-around to allow the 'Add X here' text to be a drop target in Firefox. var node = preview.one('span').getDOMNode(); node.firstChild.nodeValue = type.addmessage; }, /** * Unhide the preview information element for the given section and set it to display * the correct message * @param {Object} section the YUI node representing the selected course section */ showPreviewInfoElement: function(section) { this.hide_preview_element(); section.one('.dndupload-preview-wrapper').removeClass('dndupload-hidden'); section.addClass('dndupload-dropzone'); this.rePositionPreviewInfoElement(section); }, /** * Add the preview element to a course section. Note: this needs to be done before 'addEventListener' * is called, otherwise Firefox will ignore events generated when the mouse is over the preview * element (instead of passing them up to the parent element) * @param {Object} section the YUI node representing the selected course section */ add_preview_element: function(section) { const modsEl = this.get_mods_element(section); // Create overlay div. const overlay = document.createElement('div'); overlay.className = 'dndupload-preview-overlay'; modsEl.get('parentNode').appendChild(overlay); // Create preview div. const preview = { wrapper: document.createElement('div'), div: document.createElement('div'), iconSpan: document.createElement('span'), nameSpan: document.createElement('span') }; preview.wrapper.className = 'dndupload-preview-wrapper dndupload-hidden'; preview.wrapper.appendChild(preview.div); preview.div.className = 'dndupload-preview'; preview.div.appendChild(document.createTextNode(' ')); preview.iconSpan.className = 'fa fa-arrow-circle-o-down'; preview.nameSpan.className = 'instancename'; preview.nameSpan.innerHTML = M.util.get_string('addfilehere', 'moodle'); preview.div.appendChild(preview.iconSpan); preview.div.appendChild(preview.nameSpan); modsEl.get('parentNode').appendChild(preview.wrapper); }, /** * Re-position the preview information element by calculating the section position. * * @param {Object} section the YUI node representing the selected course section */ rePositionPreviewInfoElement: function(section) { const sectionElement = document.getElementById(section.get('id')); const rect = sectionElement.getBoundingClientRect(); const sectionHeight = parseInt(window.getComputedStyle(sectionElement).height, 10); const sectionOffset = rect.top; const preview = sectionElement.querySelector('.dndupload-preview-wrapper'); const previewHeight = parseInt(window.getComputedStyle(preview).height, 10) + (2 * parseInt(window.getComputedStyle(preview).padding, 10)); let top, bottom; if (sectionOffset < 0) { if (sectionHeight + sectionOffset >= previewHeight) { // We have enough space here, just stick the preview to the top. let offSetTop = 0 - sectionOffset; const navBar = document.querySelector('nav.navbar.fixed-top'); if (navBar) { offSetTop = offSetTop + navBar.offsetHeight; } top = offSetTop + 'px'; bottom = 'unset'; } else { // We do not have enough space here, just stick the preview to the bottom. top = 'unset'; bottom = 0; } } else { top = 0; bottom = 'unset'; } preview.style.top = top preview.style.bottom = bottom; }, /** * Find the registered handler for the given file type. If there is more than one, ask the * user which one to use. Then upload the file to the server * @param file the details of the file, taken from the FileList in the drop event * @param section the DOM element representing the selected course section * @param sectionnumber the number of the selected course section */ handle_file: function(file, section, sectionnumber) { var handlers = new Array(); var filehandlers = this.handlers.filehandlers; var extension = ''; var dotpos = file.name.lastIndexOf('.'); if (dotpos != -1) { extension = file.name.substr(dotpos+1, file.name.length).toLowerCase(); } for (var i=0; i'; content += '
'; for (var i=0; i'; content += '
'; } content += '
'; var Y = this.Y; var self = this; var panel = new M.core.dialogue({ bodyContent: content, width: '350px', modal: true, visible: false, render: true, align: { node: null, points: [Y.WidgetPositionAlign.CC, Y.WidgetPositionAlign.CC] } }); panel.show(); // When the panel is hidden - destroy it and then check for other pending uploads panel.after("visibleChange", function(e) { if (!panel.get('visible')) { panel.destroy(true); self.check_upload_queue(); } }); // Add the submit/cancel buttons to the bottom of the dialog. panel.addButton({ label: M.util.get_string('upload', 'moodle'), action: function(e) { e.preventDefault(); // Find out which module was selected var module = false; var div = Y.one('#dndupload_handlers'+uploadid); div.all('input').each(function(input) { if (input.get('checked')) { module = input.get('value'); } }); if (!module) { return; } panel.hide(); // Remember this selection for next time self.lastselected[extension] = module; // Do the upload self.upload_file(file, section, sectionnumber, module); }, section: Y.WidgetStdMod.FOOTER }); panel.addButton({ label: M.util.get_string('cancel', 'moodle'), action: function(e) { e.preventDefault(); panel.hide(); }, section: Y.WidgetStdMod.FOOTER }); }, /** * Check to see if there are any other dialog boxes to show, now that the current one has * been dealt with */ check_upload_queue: function() { this.uploaddialog = false; if (this.uploadqueue.length == 0) { return; } var details = this.uploadqueue.shift(); if (details.isfile) { this.file_handler_dialog(details.handlers, details.extension, details.file, details.section, details.sectionnumber); } else { this.handle_item(details.type, details.contents, details.section, details.sectionnumber); } }, /** * Do the file upload: show the dummy element, use an AJAX call to send the data * to the server, update the progress bar for the file, then replace the dummy * element with the real information once the AJAX call completes * @param file the details of the file, taken from the FileList in the drop event * @param section the DOM element representing the selected course section * @param sectionnumber the number of the selected course section */ upload_file: function(file, section, sectionnumber, module) { // This would be an ideal place to use the Y.io function // however, this does not support data encoded using the // FormData object, which is needed to transfer data from // the DataTransfer object into an XMLHTTPRequest // This can be converted when the YUI issue has been integrated: // http://yuilibrary.com/projects/yui3/ticket/2531274 var xhr = new XMLHttpRequest(); var self = this; if (this.maxbytes > 0 && file.size > this.maxbytes) { new M.core.alert({message: M.util.get_string('namedfiletoolarge', 'moodle', {filename: file.name})}); return; } // Add the file to the display var resel = this.add_resource_element(file.name, section, module); // Update the progress bar as the file is uploaded xhr.upload.addEventListener('progress', function(e) { if (e.lengthComputable) { var percentage = Math.round((e.loaded * 100) / e.total); resel.progress.style.width = percentage + '%'; } }, false); // Wait for the AJAX call to complete, then update the // dummy element with the returned details xhr.onreadystatechange = function() { if (xhr.readyState == 1) { this.originalUnloadEvent = window.onbeforeunload; // Trigger form upload start events. require(['core_form/events'], function(FormEvent) { FormEvent.notifyUploadStarted(section.get('id')); }); } if (xhr.readyState == 4) { if (xhr.status == 200) { var result = JSON.parse(xhr.responseText); if (result) { if (result.error == 0) { // All OK - replace the dummy element. resel.li.outerHTML = result.fullcontent; if (self.Y.UA.gecko > 0) { // Fix a Firefox bug which makes sites with a '~' in their wwwroot // log the user out when clicking on the link (before refreshing the page). resel.li.outerHTML = unescape(resel.li.outerHTML); } self.add_editing(result.elementid); // Once done, send any new course module id to the courseeditor to update de course state. self.courseeditor.dispatch('cmState', [result.cmid]); // Fire the content updated event. require(['core/event', 'jquery'], function(event, $) { event.notifyFilterContentUpdated($(result.fullcontent)); }); } else { // Error - remove the dummy element resel.parent.removeChild(resel.li); new M.core.alert({message: result.error}); } } } else { new M.core.alert({message: M.util.get_string('servererror', 'moodle')}); } // Trigger form upload complete events. require(['core_form/events'], function(FormEvent) { FormEvent.notifyUploadCompleted(section.get('id')); }); } }; // Prepare the data to send var formData = new FormData(); try { formData.append('repo_upload_file', file); } catch (e) { // Edge throws an error at this point if we try to upload a folder. resel.parent.removeChild(resel.li); new M.core.alert({message: M.util.get_string('filereaderror', 'moodle', file.name)}); return; } formData.append('sesskey', M.cfg.sesskey); formData.append('course', this.courseid); formData.append('section', sectionnumber); formData.append('module', module); formData.append('type', 'Files'); // Try reading the file to check it is not a folder, before sending it to the server. var reader = new FileReader(); reader.onload = function() { // File was read OK - send it to the server. xhr.open("POST", self.url, true); xhr.send(formData); }; reader.onerror = function() { // Unable to read the file (it is probably a folder) - display an error message. resel.parent.removeChild(resel.li); new M.core.alert({message: M.util.get_string('filereaderror', 'moodle', file.name)}); }; if (file.size > 0) { // If this is a non-empty file, try reading the first few bytes. // This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files. reader.readAsText(file.slice(0, 5)); } else { // If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(), // instead of reader.onerror(). // So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected). reader.readAsText(file); } }, /** * Show a dialog box to gather the name of the resource / activity to be created * from the uploaded content * @param type the details of the type of content * @param contents the contents to be uploaded * @section the DOM element for the section being uploaded to * @sectionnumber the number of the section being uploaded to */ handle_item: function(type, contents, section, sectionnumber) { if (type.handlers.length == 0) { // Nothing to handle this - should not have got here return; } if (type.handlers.length == 1 && type.handlers[0].noname) { // Only one handler and it doesn't need a name (i.e. a label). this.upload_item('', type.type, contents, section, sectionnumber, type.handlers[0].module); this.check_upload_queue(); return; } if (this.uploaddialog) { var details = new Object(); details.isfile = false; details.type = type; details.contents = contents; details.section = section; details.setcionnumber = sectionnumber; this.uploadqueue.push(details); return; } this.uploaddialog = true; var timestamp = new Date().getTime(); var uploadid = Math.round(Math.random()*100000)+'-'+timestamp; var nameid = 'dndupload_handler_name'+uploadid; var content = ''; if (type.handlers.length > 1) { content += '

'+type.handlermessage+'

'; content += '
'; var sel = type.handlers[0].module; for (var i=0; i'; content += '
'; } content += '
'; } var disabled = (type.handlers[0].noname) ? ' disabled = "disabled" ' : ''; content += ''; content += ' '; var Y = this.Y; var self = this; var panel = new M.core.dialogue({ bodyContent: content, width: '350px', modal: true, visible: true, render: true, align: { node: null, points: [Y.WidgetPositionAlign.CC, Y.WidgetPositionAlign.CC] } }); // When the panel is hidden - destroy it and then check for other pending uploads panel.after("visibleChange", function(e) { if (!panel.get('visible')) { panel.destroy(true); self.check_upload_queue(); } }); var namefield = Y.one('#'+nameid); var submit = function(e) { e.preventDefault(); var name = Y.Lang.trim(namefield.get('value')); var module = false; var noname = false; if (type.handlers.length > 1) { // Find out which module was selected var div = Y.one('#dndupload_handlers'+uploadid); div.all('input').each(function(input) { if (input.get('checked')) { var idx = input.get('value'); module = type.handlers[idx].module; noname = type.handlers[idx].noname; } }); if (!module) { return; } } else { module = type.handlers[0].module; noname = type.handlers[0].noname; } if (name == '' && !noname) { return; } if (noname) { name = ''; } panel.hide(); // Do the upload self.upload_item(name, type.type, contents, section, sectionnumber, module); }; // Add the submit/cancel buttons to the bottom of the dialog. panel.addButton({ label: M.util.get_string('upload', 'moodle'), action: submit, section: Y.WidgetStdMod.FOOTER, name: 'submit' }); panel.addButton({ label: M.util.get_string('cancel', 'moodle'), action: function(e) { e.preventDefault(); panel.hide(); }, section: Y.WidgetStdMod.FOOTER }); var submitbutton = panel.getButton('submit').button; namefield.on('key', submit, 'enter'); // Submit the form if 'enter' pressed namefield.after('keyup', function() { if (Y.Lang.trim(namefield.get('value')) == '') { submitbutton.disable(); } else { submitbutton.enable(); } }); // Enable / disable the 'name' box, depending on the handler selected. for (i=0; i 0) { // Fix a Firefox bug which makes sites with a '~' in their wwwroot // log the user out when clicking on the link (before refreshing the page). resel.li.outerHTML = unescape(resel.li.outerHTML); } self.add_editing(result.elementid); // Once done, send any new course module id to the courseeditor to update de course state. self.courseeditor.dispatch('cmState', [result.cmid]); } else { // Error - remove the dummy element resel.parent.removeChild(resel.li); // Trigger form upload complete events. require(['core_form/events'], function(FormEvent) { FormEvent.notifyUploadCompleted(section.get('id')); }); new M.core.alert({message: result.error}); } } } else { // Trigger form upload complete events. require(['core_form/events'], function(FormEvent) { FormEvent.notifyUploadCompleted(section.get('id')); }); new M.core.alert({message: M.util.get_string('servererror', 'moodle')}); } // Trigger form upload complete events. require(['core_form/events'], function(FormEvent) { FormEvent.notifyUploadCompleted(section.get('id')); }); } }; // Prepare the data to send var formData = new FormData(); formData.append('contents', contents); formData.append('displayname', name); formData.append('sesskey', M.cfg.sesskey); formData.append('course', this.courseid); formData.append('section', sectionnumber); formData.append('type', type); formData.append('module', module); // Send the data xhr.open("POST", this.url, true); xhr.send(formData); }, /** * Call the AJAX course editing initialisation to add the editing tools * to the newly-created resource link * @param elementid the id of the DOM element containing the new resource link * @param sectionnumber the number of the selected course section */ add_editing: function(elementid) { var node = Y.one('#' + elementid); YUI().use('moodle-course-coursebase', function(Y) { Y.log("Invoking setup_for_resource", 'debug', 'coursedndupload'); M.course.coursebase.invoke_function('setup_for_resource', node); }); if (M.core.actionmenu && M.core.actionmenu.newDOMNode) { M.core.actionmenu.newDOMNode(node); } } }; PK$9\-B))) resources.phpnu[. /** * List of all resource type modules in course * * @package moodlecore * @copyright 2009 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); $courseid = required_param('id', PARAM_INT); \core_courseformat\activityoverviewbase::redirect_to_overview_page($courseid, 'resource'); PK$9\0tags.phpnu[. /** * Edit course tags * * @package core_course * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since 5.0 */ require_once("../r.php"); PK$9\]IB\\management.phpnu[. /** * Course and category management interfaces. * * @package core_course * @copyright 2013 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once($CFG->dirroot.'/course/lib.php'); $categoryid = optional_param('categoryid', null, PARAM_INT); $selectedcategoryid = optional_param('selectedcategoryid', null, PARAM_INT); $courseid = optional_param('courseid', null, PARAM_INT); $action = optional_param('action', false, PARAM_ALPHA); $page = optional_param('page', 0, PARAM_INT); $perpage = optional_param('perpage', null, PARAM_INT); $viewmode = optional_param('view', 'default', PARAM_ALPHA); // Can be one of default, combined, courses, or categories. // Search related params. $search = optional_param('search', '', PARAM_RAW); // Search words. Shortname, fullname, idnumber and summary get searched. $blocklist = optional_param('blocklist', 0, PARAM_INT); // Find courses containing this block. $modulelist = optional_param('modulelist', '', PARAM_PLUGIN); // Find courses containing the given modules. if (!in_array($viewmode, array('default', 'combined', 'courses', 'categories'))) { $viewmode = 'default'; } $issearching = ($search !== '' || $blocklist !== 0 || $modulelist !== ''); if ($issearching) { $viewmode = 'courses'; } $url = new moodle_url('/course/management.php'); $systemcontext = $context = context_system::instance(); if ($courseid) { $record = get_course($courseid); $course = new core_course_list_element($record); $category = core_course_category::get($course->category); $categoryid = $category->id; $context = context_coursecat::instance($category->id); $url->param('categoryid', $categoryid); $url->param('courseid', $course->id); } else if ($categoryid) { $courseid = null; $course = null; $category = core_course_category::get($categoryid); $context = context_coursecat::instance($category->id); $url->param('categoryid', $category->id); } else { $course = null; $courseid = null; $topchildren = core_course_category::top()->get_children(); if (empty($topchildren)) { throw new moodle_exception('cannotviewcategory', 'error'); } $category = reset($topchildren); $categoryid = $category->id; $context = context_coursecat::instance($category->id); $url->param('categoryid', $category->id); } // Check if there is a selected category param, and if there is apply it. if ($course === null && $selectedcategoryid !== null && $selectedcategoryid !== $categoryid) { $url->param('categoryid', $selectedcategoryid); } if ($page !== 0) { $url->param('page', $page); } if ($viewmode !== 'default') { $url->param('view', $viewmode); } if ($search !== '') { $url->param('search', $search); } if ($blocklist !== 0) { $url->param('blocklist', $search); } if ($modulelist !== '') { $url->param('modulelist', $search); } $strmanagement = new lang_string('coursecatmanagement'); $pageheading = $category->get_formatted_name(); $PAGE->set_context($context); $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); $PAGE->set_title($strmanagement); $PAGE->set_heading($pageheading); $PAGE->requires->js_call_amd('core_course/copy_modal', 'init', array($context->id)); $PAGE->set_secondary_active_tab('categorymain'); // This is a system level page that operates on other contexts. require_login(); if (!core_course_category::has_capability_on_any(array('moodle/category:manage', 'moodle/course:create'))) { // The user isn't able to manage any categories. Lets redirect them to the relevant course/index.php page. $url = new moodle_url('/course/index.php'); if ($categoryid) { $url->param('categoryid', $categoryid); } redirect($url); } if (!$issearching && $category !== null) { $parents = core_course_category::get_many($category->get_parents()); $parents[] = $category; foreach ($parents as $parent) { $PAGE->navbar->add( $parent->get_formatted_name(), new moodle_url('/course/index.php', array('categoryid' => $parent->id)) ); } if ($course instanceof core_course_list_element) { // Use the list name so that it matches whats being displayed below. $PAGE->navbar->add($course->get_formatted_name()); } } // If the user poses any of these capabilities then they will be able to see the admin // tree and the management link within it. // This is the most accurate form of navigation. $capabilities = array( 'moodle/site:config', 'moodle/backup:backupcourse', 'moodle/category:manage', 'moodle/course:create', 'moodle/site:approvecourse' ); if ($category && !has_any_capability($capabilities, $systemcontext)) { // If the user doesn't poses any of these system capabilities then we're going to mark the category link in the // settings block as active, tell the page to ignore the active path and just build what the user would expect. // This will at least give the page some relevant navigation. navigation_node::override_active_url(new moodle_url('/course/index.php', array('categoryid' => $category->id))); $PAGE->set_category_by_id($category->id); $PAGE->navbar->ignore_active(true); } else { // If user has system capabilities, make sure the "Category" item in Administration block is active. navigation_node::require_admin_tree(); navigation_node::override_active_url(new moodle_url('/course/index.php')); } $PAGE->navbar->add(get_string('coursemgmt', 'admin'), $PAGE->url->out_omit_querystring()); $PAGE->set_primary_active_tab('home'); $notificationspass = array(); $notificationsfail = array(); if ($action !== false && confirm_sesskey()) { // Actions: // - resortcategories : Resort the courses in the given category. // - resortcourses : Resort courses // - showcourse : make a course visible. // - hidecourse : make a course hidden. // - movecourseup : move the selected course up one. // - movecoursedown : move the selected course down. // - showcategory : make a category visible. // - hidecategory : make a category hidden. // - movecategoryup : move category up. // - movecategorydown : move category down. // - deletecategory : delete the category either in full, or moving contents. // - bulkaction : performs bulk actions: // - bulkmovecourses. // - bulkmovecategories. // - bulkresortcategories. $redirectback = false; $redirectmessage = false; switch ($action) { case 'resortcategories' : $sort = required_param('resort', PARAM_ALPHA); $cattosort = core_course_category::get((int)optional_param('categoryid', 0, PARAM_INT)); $redirectback = \core_course\management\helper::action_category_resort_subcategories($cattosort, $sort); break; case 'resortcourses' : // They must have specified a category. required_param('categoryid', PARAM_INT); $sort = required_param('resort', PARAM_ALPHA); \core_course\management\helper::action_category_resort_courses($category, $sort); break; case 'showcourse' : $redirectback = \core_course\management\helper::action_course_show($course); break; case 'hidecourse' : $redirectback = \core_course\management\helper::action_course_hide($course); break; case 'movecourseup' : // They must have specified a category and a course. required_param('categoryid', PARAM_INT); required_param('courseid', PARAM_INT); $redirectback = \core_course\management\helper::action_course_change_sortorder_up_one($course, $category); break; case 'movecoursedown' : // They must have specified a category and a course. required_param('categoryid', PARAM_INT); required_param('courseid', PARAM_INT); $redirectback = \core_course\management\helper::action_course_change_sortorder_down_one($course, $category); break; case 'showcategory' : // They must have specified a category. required_param('categoryid', PARAM_INT); $redirectback = \core_course\management\helper::action_category_show($category); break; case 'hidecategory' : // They must have specified a category. required_param('categoryid', PARAM_INT); $redirectback = \core_course\management\helper::action_category_hide($category); break; case 'movecategoryup' : // They must have specified a category. required_param('categoryid', PARAM_INT); $redirectback = \core_course\management\helper::action_category_change_sortorder_up_one($category); break; case 'movecategorydown' : // They must have specified a category. required_param('categoryid', PARAM_INT); $redirectback = \core_course\management\helper::action_category_change_sortorder_down_one($category); break; case 'deletecategory': // They must have specified a category. required_param('categoryid', PARAM_INT); if (!$category->can_delete()) { throw new moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_resort'); } $mform = new core_course_deletecategory_form(null, $category); if ($mform->is_cancelled()) { redirect($PAGE->url); } // Start output. /* @var core_course_management_renderer|core_renderer $renderer */ $renderer = $PAGE->get_renderer('core_course', 'management'); echo $renderer->header(); echo $renderer->heading(get_string('deletecategory', 'moodle', $category->get_formatted_name())); if ($data = $mform->get_data()) { // The form has been submit handle it. if ($data->fulldelete == 1 && $category->can_delete_full()) { $continueurl = new moodle_url('/course/management.php'); if ($category->parent != '0') { $continueurl->param('categoryid', $category->parent); } $notification = get_string('coursecategorydeleted', '', $category->get_formatted_name()); $deletedcourses = $category->delete_full(true); foreach ($deletedcourses as $course) { echo $renderer->notification(get_string('coursedeleted', '', $course->shortname), 'notifysuccess'); } echo $renderer->notification($notification, 'notifysuccess'); echo $renderer->continue_button($continueurl); } else if ($data->fulldelete == 0 && $category->can_move_content_to($data->newparent)) { $continueurl = new moodle_url('/course/management.php', array('categoryid' => $data->newparent)); $category->delete_move($data->newparent, true); echo $renderer->continue_button($continueurl); } else { // Some error in parameters (user is cheating?) $mform->display(); } } else { // Display the form. $mform->display(); } // Finish output and exit. echo $renderer->footer(); exit(); break; case 'bulkaction': $bulkmovecourses = optional_param('bulkmovecourses', false, PARAM_BOOL); $bulkmovecategories = optional_param('bulkmovecategories', false, PARAM_BOOL); $bulkresortcategories = optional_param('bulksort', false, PARAM_BOOL); if ($bulkmovecourses) { // Move courses out of the current category and into a new category. // They must have specified a category. required_param('categoryid', PARAM_INT); $movetoid = required_param('movecoursesto', PARAM_INT); $courseids = optional_param_array('bc', false, PARAM_INT); if ($courseids === false) { break; } $moveto = core_course_category::get($movetoid); try { // If this fails we want to catch the exception and report it. $redirectback = \core_course\management\helper::move_courses_into_category($moveto, $courseids); if ($redirectback) { $a = new stdClass; $a->category = $moveto->get_formatted_name(); $a->courses = count($courseids); $redirectmessage = get_string('bulkmovecoursessuccess', 'moodle', $a); } } catch (moodle_exception $ex) { $redirectback = false; $notificationsfail[] = $ex->getMessage(); } } else if ($bulkmovecategories) { $categoryids = optional_param_array('bcat', array(), PARAM_INT); $movetocatid = required_param('movecategoriesto', PARAM_INT); $movetocat = core_course_category::get($movetocatid); $movecount = 0; foreach ($categoryids as $id) { $cattomove = core_course_category::get($id); if ($id == $movetocatid) { $notificationsfail[] = get_string('movecategoryownparent', 'error', $cattomove->get_formatted_name()); continue; } // Don't allow user to move selected category into one of it's own sub-categories. if (strpos($movetocat->path, $cattomove->path . '/') === 0) { $notificationsfail[] = get_string('movecategoryparentconflict', 'error', $cattomove->get_formatted_name()); continue; } if ($cattomove->parent != $movetocatid) { if ($cattomove->can_change_parent($movetocatid)) { $cattomove->change_parent($movetocatid); $movecount++; } else { $notificationsfail[] = get_string('movecategorynotpossible', 'error', $cattomove->get_formatted_name()); } } } if ($movecount > 1) { $a = new stdClass; $a->count = $movecount; $a->to = $movetocat->get_formatted_name(); $movesuccessstrkey = 'movecategoriessuccess'; if ($movetocatid == 0) { $movesuccessstrkey = 'movecategoriestotopsuccess'; } $notificationspass[] = get_string($movesuccessstrkey, 'moodle', $a); } else if ($movecount === 1) { $a = new stdClass; $a->moved = $cattomove->get_formatted_name(); $a->to = $movetocat->get_formatted_name(); $movesuccessstrkey = 'movecategorysuccess'; if ($movetocatid == 0) { $movesuccessstrkey = 'movecategorytotopsuccess'; } $notificationspass[] = get_string($movesuccessstrkey, 'moodle', $a); } } else if ($bulkresortcategories) { $for = required_param('selectsortby', PARAM_ALPHA); $sortcategoriesby = required_param('resortcategoriesby', PARAM_ALPHA); $sortcoursesby = required_param('resortcoursesby', PARAM_ALPHA); if ($sortcategoriesby === 'none' && $sortcoursesby === 'none') { // They're not sorting anything. break; } if (!in_array($sortcategoriesby, array('idnumber', 'idnumberdesc', 'name', 'namedesc'))) { $sortcategoriesby = false; } if (!in_array($sortcoursesby, array('timecreated', 'timecreateddesc', 'idnumber', 'idnumberdesc', 'fullname', 'fullnamedesc', 'shortname', 'shortnamedesc'))) { $sortcoursesby = false; } if ($for === 'thiscategory') { $categoryids = array( required_param('currentcategoryid', PARAM_INT) ); $categories = core_course_category::get_many($categoryids); } else if ($for === 'selectedcategories') { // Bulk resort selected categories. $categoryids = optional_param_array('bcat', false, PARAM_INT); $sort = required_param('resortcategoriesby', PARAM_ALPHA); if ($categoryids === false) { break; } $categories = core_course_category::get_many($categoryids); } else if ($for === 'allcategories') { if ($sortcategoriesby && core_course_category::top()->can_resort_subcategories()) { \core_course\management\helper::action_category_resort_subcategories( core_course_category::top(), $sortcategoriesby); } $categorieslist = core_course_category::make_categories_list('moodle/category:manage'); $categoryids = array_keys($categorieslist); $categories = core_course_category::get_many($categoryids); unset($categorieslist); } else { break; } foreach ($categories as $cat) { if ($sortcategoriesby && $cat->can_resort_subcategories()) { // Don't clean up here, we'll do it once we're all done. \core_course\management\helper::action_category_resort_subcategories($cat, $sortcategoriesby, false); } if ($sortcoursesby && $cat->can_resort_courses()) { \core_course\management\helper::action_category_resort_courses($cat, $sortcoursesby, false); } } core_course_category::resort_categories_cleanup($sortcoursesby !== false); if ($category === null && count($categoryids) === 1) { // They're bulk sorting just a single category and they've not selected a category. // Lets for convenience sake auto-select the category that has been resorted for them. redirect(new moodle_url($PAGE->url, array('categoryid' => reset($categoryids)))); } } } if ($redirectback) { if ($redirectmessage) { redirect($PAGE->url, $redirectmessage, 5); } else { redirect($PAGE->url); } } } if (!is_null($perpage)) { set_user_preference('coursecat_management_perpage', $perpage); } else { $perpage = get_user_preferences('coursecat_management_perpage', $CFG->coursesperpage); } if ((int)$perpage != $perpage || $perpage < 2) { $perpage = $CFG->coursesperpage; } $categorysize = 4; $coursesize = 4; $detailssize = 4; if ($viewmode === 'default' || $viewmode === 'combined') { if (isset($courseid)) { $class = 'columns-3'; } else { $categorysize = 5; $coursesize = 7; $class = 'columns-2'; } } else if ($viewmode === 'categories') { $categorysize = 12; $class = 'columns-1'; } else if ($viewmode === 'courses') { if (isset($courseid)) { $coursesize = 6; $detailssize = 6; $class = 'columns-2'; } else { $coursesize = 12; $class = 'columns-1'; } } if ($viewmode === 'default' || $viewmode === 'combined') { $class .= ' viewmode-combined'; } else { $class .= ' viewmode-'.$viewmode; } if (($viewmode === 'default' || $viewmode === 'combined' || $viewmode === 'courses') && !empty($courseid)) { $class .= ' course-selected'; } /* @var core_course_management_renderer|core_renderer $renderer */ $renderer = $PAGE->get_renderer('core_course', 'management'); $renderer->enhance_management_interface(); $displaycategorylisting = ($viewmode === 'default' || $viewmode === 'combined' || $viewmode === 'categories'); $displaycourselisting = ($viewmode === 'default' || $viewmode === 'combined' || $viewmode === 'courses'); $displaycoursedetail = (isset($courseid)); echo $renderer->header(); $actionbar = new \core_course\output\manage_categories_action_bar($PAGE, $viewmode, $course, $search); echo $renderer->render_action_bar($actionbar); if (count($notificationspass) > 0) { echo $renderer->notification(join('
', $notificationspass), 'notifysuccess'); } if (count($notificationsfail) > 0) { echo $renderer->notification(join('
', $notificationsfail)); } // Start the management form. echo $renderer->management_form_start(); echo $renderer->accessible_skipto_links($displaycategorylisting, $displaycourselisting, $displaycoursedetail); echo $renderer->grid_start('course-category-listings', $class); if ($displaycategorylisting) { echo $renderer->grid_column_start($categorysize, 'category-listing'); echo $renderer->category_listing($category); echo $renderer->grid_column_end(); } if ($displaycourselisting) { echo $renderer->grid_column_start($coursesize, 'course-listing'); if (!$issearching) { echo $renderer->course_listing($category, $course, $page, $perpage, $viewmode); } else { list($courses, $coursescount, $coursestotal) = \core_course\management\helper::search_courses($search, $blocklist, $modulelist, $page, $perpage); echo $renderer->search_listing($courses, $coursestotal, $course, $page, $perpage, $search); } echo $renderer->grid_column_end(); if ($displaycoursedetail) { echo $renderer->grid_column_start($detailssize, 'course-detail'); echo $renderer->course_detail($course); echo $renderer->grid_column_end(); } } echo $renderer->grid_end(); // End of the management form. echo $renderer->management_form_end(); echo $renderer->footer(); PK$9\D*&editcategory.phpnu[. /** * Page for creating or editing course category name/parent/description. * * When called with an id parameter, edits the category with that id. * Otherwise it creates a new category with default parent from the parent * parameter, which may be 0. * * @package core_course * @copyright 2007 Nicolas Connault * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once($CFG->dirroot.'/course/lib.php'); require_login(); $id = optional_param('id', 0, PARAM_INT); $url = new moodle_url('/course/editcategory.php'); if ($id) { $coursecat = core_course_category::get($id, MUST_EXIST, true); $category = $coursecat->get_db_record(); $context = context_coursecat::instance($id); navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $category->id])); $PAGE->navbar->add(get_string('settings')); $PAGE->set_primary_active_tab('home'); $PAGE->set_secondary_active_tab('edit'); $url->param('id', $id); $strtitle = new lang_string('editcategorysettings'); $itemid = 0; // Initialise itemid, as all files in category description has item id 0. $title = $strtitle; $fullname = $coursecat->get_formatted_name(); } else { $parent = required_param('parent', PARAM_INT); $url->param('parent', $parent); $strtitle = get_string('addnewcategory'); if ($parent) { $parentcategory = $DB->get_record('course_categories', array('id' => $parent), '*', MUST_EXIST); $context = context_coursecat::instance($parent); navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $parent])); $fullname = format_string($parentcategory->name, true, ['context' => $context->id]); $title = "$fullname: $strtitle"; $managementurl = new moodle_url('/course/management.php'); // These are the caps required in order to see the management interface. $managementcaps = array('moodle/category:manage', 'moodle/course:create'); if (!has_any_capability($managementcaps, context_system::instance())) { // If the user doesn't have either manage caps then they can only manage within the given category. $managementurl->param('categoryid', $parent); } $PAGE->set_primary_active_tab('home'); $PAGE->navbar->add(get_string('coursemgmt', 'admin'), $managementurl); $PAGE->navbar->add(get_string('addcategory', 'admin')); } else { $context = context_system::instance(); $fullname = $SITE->fullname; $title = $strtitle; $PAGE->set_secondary_active_tab('courses'); } $category = new stdClass(); $category->id = 0; $category->parent = $parent; $itemid = null; // Set this explicitly, so files for parent category should not get loaded in draft area. } require_capability('moodle/category:manage', $context); $PAGE->set_context($context); $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); $PAGE->set_title($title); $PAGE->set_heading($fullname); $mform = new core_course_editcategory_form(null, array( 'categoryid' => $id, 'parent' => $category->parent, 'context' => $context, 'itemid' => $itemid )); $mform->set_data(file_prepare_standard_editor( $category, 'description', $mform->get_description_editor_options(), $context, 'coursecat', 'description', $itemid )); $manageurl = new moodle_url('/course/management.php'); if ($mform->is_cancelled()) { if ($id) { $manageurl->param('categoryid', $id); } else if ($parent) { $manageurl->param('categoryid', $parent); } redirect($manageurl); } else if ($data = $mform->get_data()) { if (isset($coursecat)) { if ((int)$data->parent !== (int)$coursecat->parent && !$coursecat->can_change_parent($data->parent)) { throw new \moodle_exception('cannotmovecategory'); } $coursecat->update($data, $mform->get_description_editor_options()); } else { $category = core_course_category::create($data, $mform->get_description_editor_options()); } $manageurl->param('categoryid', $category->id); redirect($manageurl); } echo $OUTPUT->header(); echo $OUTPUT->heading($strtitle); $mform->display(); echo $OUTPUT->footer(); PK$9\Wmodregrade.phpnu[. /** * Regrades a module that has been added or updated when it might take a long time. * * A progress bar will be displayed in that case, and then the user can click 'Continue' to proceed. * If for some reason you get to this page when regrading will not take a long time, then it will * redirect immediately. * * @package core_course * @copyright 2022 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_OUTPUT_BUFFERING', true); require('../config.php'); require_once($CFG->libdir . '/gradelib.php'); $cmid = required_param('id', PARAM_INT); $urlpath = required_param('url', PARAM_LOCALURL); $url = new moodle_url($urlpath); // Check that an absolute url/path was specified e.g. /course/view.php (it probably isn't a // security risk to allow a relative one, but isn't necessary here). if (!$url->get_host()) { throw new moodle_exception('missingparam', 'error', '', 'url'); } // Get course. [$course, $cm] = get_course_and_cm_from_cmid($cmid); $context = \context_module::instance($cm->id); // Set up page for display. $PAGE->set_url(new moodle_url('/course/modregrade.php', ['id' => $cmid, 'url' => $url])); $PAGE->set_context($context); $PAGE->set_title($course->shortname . ': ' . get_string('recalculatinggrades', 'grades')); // Security check: must be logged in with manage activities permission. require_login($course, false, $cm); require_capability('moodle/course:manageactivities', $context); // Do the regrade if necessary. grade_regrade_final_grades_if_required($course); // Redirect back to the target URL. redirect($url); PK$9\E %be e format/topics/db/upgrade.phpnu[. /** * Upgrade scripts for Topics course format. * * @package format_topics * @copyright 2017 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Upgrade script for Topics course format. * * @param int|float $oldversion the version we are upgrading from * @return bool result */ function xmldb_format_topics_upgrade($oldversion) { global $DB; // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2023100901) { // During the migration to version 4.4, ensure that sections with null names are renamed to their corresponding // previous 'Topic X' for continuity. $newsectionname = $DB->sql_concat("'Topic '", 'section'); $sql = << 0 AND (name IS NULL OR name = '') AND course IN (SELECT id FROM {course} WHERE format = 'topics') EOF; $DB->execute( sql: $sql, ); // Main savepoint reached. upgrade_plugin_savepoint(true, 2023100901, 'format', 'topics'); } // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } PK$9\ Yˮ'format/topics/lang/en/format_topics.phpnu[. /** * Strings for component Custom sections course format. * * @package format_topics * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['currentsection'] = 'Current section'; $string['hidefromothers'] = 'Hide'; $string['legacysectionname'] = 'Topic'; $string['newsection'] = 'New section'; $string['page-course-view-topics'] = 'Any course main page in custom sections format'; $string['page-course-view-topics-x'] = 'Any course page in custom sections format'; $string['pluginname'] = 'Custom sections'; $string['plugin_description'] = 'The course is divided into customisable sections.'; $string['privacy:metadata'] = 'The Custom sections format plugin does not store any personal data.'; $string['indentation'] = 'Allow indentation on course page'; $string['indentation_help'] = 'Allow teachers, and other users with the manage activities capability, to indent items on the course page.'; $string['section_highlight_feedback'] = 'Section {$a->name} highlighted.'; $string['section_unhighlight_feedback'] = 'Highlighting removed from section {$a->name}.'; $string['section0name'] = 'General'; $string['sectionname'] = 'Section'; $string['showfromothers'] = 'Show'; PK$9\i  Cformat/topics/backup/moodle2/restore_format_topics_plugin.class.phpnu[. /** * Specialised restore for Topics course format. * * @package format_topics * @category backup * @copyright 2017 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Specialised restore for Topics course format. * * Processes 'numsections' from the old backup files and hides sections that used to be "orphaned". * * @package format_topics * @category backup * @copyright 2017 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_format_topics_plugin extends restore_format_plugin { /** @var int */ protected $originalnumsections = 0; /** * Checks if backup file was made on Moodle before 3.3 and we should respect the 'numsections' * and potential "orphaned" sections in the end of the course. * * @return bool */ protected function need_restore_numsections() { $backupinfo = $this->step->get_task()->get_info(); $backuprelease = $backupinfo->backup_release; // The major version: 2.9, 3.0, 3.10... return version_compare($backuprelease, '3.3', '<'); } /** * Creates a dummy path element in order to be able to execute code after restore. * * @return restore_path_element[] */ public function define_course_plugin_structure() { global $DB; // Since this method is executed before the restore we can do some pre-checks here. // In case of merging backup into existing course find the current number of sections. $target = $this->step->get_task()->get_target(); if (($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING) && $this->need_restore_numsections()) { $maxsection = $DB->get_field_sql( 'SELECT max(section) FROM {course_sections} WHERE course = ?', [$this->step->get_task()->get_courseid()]); $this->originalnumsections = (int)$maxsection; } // Dummy path element is needed in order for after_restore_course() to be called. return [new restore_path_element('dummy_course', $this->get_pathfor('/dummycourse'))]; } /** * Dummy process method. * * @return void */ public function process_dummy_course() { } /** * Executed after course restore is complete. * * This method is only executed if course configuration was overridden. * * @return void */ public function after_restore_course() { global $DB; if (!$this->need_restore_numsections()) { // Backup file was made in Moodle 3.3 or later, we don't need to process 'numsecitons'. return; } $data = $this->connectionpoint->get_data(); $backupinfo = $this->step->get_task()->get_info(); if ($backupinfo->original_course_format !== 'topics' || !isset($data['tags']['numsections'])) { // Backup from another course format or backup file does not even have 'numsections'. return; } $numsections = (int)$data['tags']['numsections']; foreach ($backupinfo->sections as $key => $section) { // For each section from the backup file check if it was restored and if was "orphaned" in the original // course and mark it as hidden. This will leave all activities in it visible and available just as it was // in the original course. // Exception is when we restore with merging and the course already had a section with this section number, // in this case we don't modify the visibility. if ($this->step->get_task()->get_setting_value($key . '_included')) { $sectionnum = (int)$section->title; if ($sectionnum > $numsections && $sectionnum > $this->originalnumsections) { $DB->execute("UPDATE {course_sections} SET visible = 0 WHERE course = ? AND section = ?", [$this->step->get_task()->get_courseid(), $sectionnum]); } } } } } PK$9\u?eCeCformat/topics/lib.phpnu[. /** * This file contains main class for Topics course format. * * @since Moodle 2.0 * @package format_topics * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot. '/course/format/lib.php'); use core\output\inplace_editable; /** * Main class for the Topics course format. * * @package format_topics * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class format_topics extends core_courseformat\base { /** * Returns true if this course format uses sections. * * @return bool */ public function uses_sections() { return true; } public function uses_course_index() { return true; } public function uses_indentation(): bool { return (get_config('format_topics', 'indentation')) ? true : false; } /** * Returns the display name of the given section that the course prefers. * * Use section name is specified by user. Otherwise use default ("Topic #"). * * @param int|stdClass $section Section object from database or just field section.section * @return string Display name that the course format prefers, e.g. "Topic 2" */ public function get_section_name($section) { $section = $this->get_section($section); if ((string)$section->name !== '') { return format_string($section->name, true, ['context' => context_course::instance($this->courseid)]); } else { return $this->get_default_section_name($section); } } /** * Returns the default section name for the topics course format. * * If the section number is 0, it will use the string with key = section0name from the course format's lang file. * If the section number is not 0, it will consistently return the name 'newsection', disregarding the specific section number. * * @param int|stdClass $section Section object from database or just field course_sections section * @return string The default value for the section name. */ public function get_default_section_name($section) { $section = $this->get_section($section); if ($section->sectionnum == 0) { return get_string('section0name', 'format_topics'); } return get_string('newsection', 'format_topics'); } /** * Generate the title for this section page. * * @return string the page title */ public function page_title(): string { return get_string('sectionoutline'); } /** * The URL to use for the specified course (with section). * * @param int|stdClass $section Section object from database or just field course_sections.section * if omitted the course view page is returned * @param array $options options for view URL. At the moment core uses: * 'navigation' (bool) if true and section not empty, the function returns section page; otherwise, it returns course page. * 'sr' (int) used by course formats to specify to which section to return * @return moodle_url */ public function get_view_url($section, $options = []) { $course = $this->get_course(); if (array_key_exists('sr', $options) && !is_null($options['sr'])) { $sectionno = $options['sr']; } else if (is_object($section)) { $sectionno = $section->section; } else { $sectionno = $section; } if ((!empty($options['navigation']) || array_key_exists('sr', $options)) && $sectionno !== null) { // Display section on separate page. $sectioninfo = $this->get_section($sectionno); return new moodle_url('/course/section.php', ['id' => $sectioninfo->id]); } return new moodle_url('/course/view.php', ['id' => $course->id]); } /** * Returns the information about the ajax support in the given source format. * * The returned object's property (boolean)capable indicates that * the course format supports Moodle course ajax features. * * @return stdClass */ public function supports_ajax() { $ajaxsupport = new stdClass(); $ajaxsupport->capable = true; return $ajaxsupport; } public function supports_components() { return true; } /** * Loads all of the course sections into the navigation. * * @param global_navigation $navigation * @param navigation_node $node The course node within the navigation * @return void */ public function extend_course_navigation($navigation, navigation_node $node) { global $PAGE; // If section is specified in course/view.php, make sure it is expanded in navigation. if ($navigation->includesectionnum === false) { $selectedsection = optional_param('section', null, PARAM_INT); if ($selectedsection !== null && (!defined('AJAX_SCRIPT') || AJAX_SCRIPT == '0') && $PAGE->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) { $navigation->includesectionnum = $selectedsection; } } // Check if there are callbacks to extend course navigation. parent::extend_course_navigation($navigation, $node); // We want to remove the general section if it is empty. $modinfo = get_fast_modinfo($this->get_course()); $sections = $modinfo->get_sections(); if (!isset($sections[0])) { // The general section is empty to find the navigation node for it we need to get its ID. $section = $modinfo->get_section_info(0); $generalsection = $node->get($section->id, navigation_node::TYPE_SECTION); if ($generalsection) { // We found the node - now remove it. $generalsection->remove(); } } } /** * Custom action after section has been moved in AJAX mode. * * Used in course/rest.php * * @return array This will be passed in ajax respose */ public function ajax_section_move() { global $PAGE; $titles = []; $course = $this->get_course(); $modinfo = get_fast_modinfo($course); $renderer = $this->get_renderer($PAGE); if ($renderer && ($sections = $modinfo->get_section_info_all())) { foreach ($sections as $number => $section) { $titles[$number] = $renderer->section_title($section, $course); } } return ['sectiontitles' => $titles, 'action' => 'move']; } /** * Returns the list of blocks to be automatically added for the newly created course. * * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT * each of values is an array of block names (for left and right side columns) */ public function get_default_blocks() { return [ BLOCK_POS_LEFT => [], BLOCK_POS_RIGHT => [], ]; } /** * Definitions of the additional options that this course format uses for course. * * Topics format uses the following options: * - coursedisplay * - hiddensections * * @param bool $foreditform * @return array of options */ public function course_format_options($foreditform = false) { static $courseformatoptions = false; if ($courseformatoptions === false) { $courseconfig = get_config('moodlecourse'); $courseformatoptions = [ 'hiddensections' => [ 'default' => $courseconfig->hiddensections, 'type' => PARAM_INT, ], 'coursedisplay' => [ 'default' => $courseconfig->coursedisplay, 'type' => PARAM_INT, ], ]; } if ($foreditform && !isset($courseformatoptions['coursedisplay']['label'])) { $courseformatoptionsedit = [ 'hiddensections' => [ 'label' => new lang_string('hiddensections'), 'help' => 'hiddensections', 'help_component' => 'moodle', 'element_type' => 'select', 'element_attributes' => [ [ 0 => new lang_string('hiddensectionscollapsed'), 1 => new lang_string('hiddensectionsinvisible') ], ], ], 'coursedisplay' => [ 'label' => new lang_string('coursedisplay'), 'element_type' => 'select', 'element_attributes' => [ [ COURSE_DISPLAY_SINGLEPAGE => new lang_string('coursedisplay_single'), COURSE_DISPLAY_MULTIPAGE => new lang_string('coursedisplay_multi'), ], ], 'help' => 'coursedisplay', 'help_component' => 'moodle', ], ]; $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit); } return $courseformatoptions; } /** * Adds format options elements to the course/section edit form. * * This function is called from {@link course_edit_form::definition_after_data()}. * * @param MoodleQuickForm $mform form the elements are added to. * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form. * @return array array of references to the added form elements. */ public function create_edit_form_elements(&$mform, $forsection = false) { global $COURSE; $elements = parent::create_edit_form_elements($mform, $forsection); if (!$forsection && (empty($COURSE->id) || $COURSE->id == SITEID)) { // Add "numsections" element to the create course form - it will force new course to be prepopulated // with empty sections. // The "Number of sections" option is no longer available when editing course, instead teachers should // delete and add sections when needed. $courseconfig = get_config('moodlecourse'); $max = (int)$courseconfig->maxsections; $element = $mform->addElement('select', 'numsections', get_string('numberweeks'), range(0, $max ?: 52)); $mform->setType('numsections', PARAM_INT); if (is_null($mform->getElementValue('numsections'))) { $mform->setDefault('numsections', $courseconfig->numsections); } array_unshift($elements, $element); } return $elements; } /** * Updates format options for a course. * * In case if course format was changed to 'topics', we try to copy options * 'coursedisplay' and 'hiddensections' from the previous format. * * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data * @param stdClass $oldcourse if this function is called from {@link update_course()} * this object contains information about the course before update * @return bool whether there were any changes to the options values */ public function update_course_format_options($data, $oldcourse = null) { $data = (array)$data; if ($oldcourse !== null) { $oldcourse = (array)$oldcourse; $options = $this->course_format_options(); foreach ($options as $key => $unused) { if (!array_key_exists($key, $data)) { if (array_key_exists($key, $oldcourse)) { $data[$key] = $oldcourse[$key]; } } } } return $this->update_format_options($data); } /** * Whether this format allows to delete sections. * * Do not call this function directly, instead use {@link course_can_delete_section()} * * @param int|stdClass|section_info $section * @return bool */ public function can_delete_section($section) { return true; } /** * Indicates whether the course format supports the creation of a news forum. * * @return bool */ public function supports_news() { return true; } /** * Returns whether this course format allows the activity to * have "triple visibility state" - visible always, hidden on course page but available, hidden. * * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module) * @param stdClass|section_info $section section where this module is located or will be added to * @return bool */ public function allow_stealth_module_visibility($cm, $section) { // Allow the third visibility state inside visible sections or in section 0. return !$section->section || $section->visible; } /** * Callback used in WS core_course_edit_section when teacher performs an AJAX action on a section (show/hide). * * Access to the course is already validated in the WS but the callback has to make sure * that particular action is allowed by checking capabilities * * Course formats should register. * * @param section_info|stdClass $section * @param string $action * @param int $sr * @return null|array any data for the Javascript post-processor (must be json-encodeable) */ public function section_action($section, $action, $sr) { global $PAGE; if ($section->section && ($action === 'setmarker' || $action === 'removemarker')) { // Format 'topics' allows to set and remove markers in addition to common section actions. require_capability('moodle/course:setcurrentsection', context_course::instance($this->courseid)); course_set_marker($this->courseid, ($action === 'setmarker') ? $section->section : 0); return null; } // For show/hide actions call the parent method and return the new content for .section_availability element. $rv = parent::section_action($section, $action, $sr); $renderer = $PAGE->get_renderer('format_topics'); if (!($section instanceof section_info)) { $modinfo = course_modinfo::instance($this->courseid); $section = $modinfo->get_section_info($section->section); } $elementclass = $this->get_output_classname('content\\section\\availability'); $availability = new $elementclass($this, $section); $rv['section_availability'] = $renderer->render($availability); return $rv; } /** * Return the plugin configs for external functions. * * @return array the list of configuration settings * @since Moodle 3.5 */ public function get_config_for_external() { // Return everything (nothing to hide). $formatoptions = $this->get_format_options(); $formatoptions['indentation'] = get_config('format_topics', 'indentation'); return $formatoptions; } /** * Get the required javascript files for the course format. * * @return array The list of javascript files required by the course format. */ public function get_required_jsfiles(): array { return []; } } /** * Implements callback inplace_editable() allowing to edit values in-place. * * @param string $itemtype * @param int $itemid * @param mixed $newvalue * @return inplace_editable */ function format_topics_inplace_editable($itemtype, $itemid, $newvalue) { global $DB, $CFG; require_once($CFG->dirroot . '/course/lib.php'); if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') { $section = $DB->get_record_sql( 'SELECT s.* FROM {course_sections} s JOIN {course} c ON s.course = c.id WHERE s.id = ? AND c.format = ?', [$itemid, 'topics'], MUST_EXIST); return course_get_format($section->course)->inplace_editable_update_section_name($section, $itemtype, $newvalue); } } PK$9\format/topics/version.phpnu[. /** * Version details. * * @package format_topics * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @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 = 'format_topics'; // Full name of the plugin (used for diagnostics). PK$9\TǑ*format/topics/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for Topics course format. * * @package format_topics * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace format_topics\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\metadata\null_provider; /** * Privacy Subsystem for Topics course format implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements 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$9\^1! ! )format/topics/classes/output/renderer.phpnu[. namespace format_topics\output; use core_courseformat\output\section_renderer; use moodle_page; /** * Basic renderer for topics format. * * @copyright 2012 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends section_renderer { /** * Constructor method, calls the parent constructor. * * @param moodle_page $page * @param string $target one of rendering target constants */ public function __construct(moodle_page $page, $target) { parent::__construct($page, $target); // Since format_topics_renderer::section_edit_control_items() only displays the 'Highlight' control // when editing mode is on we need to be sure that the link 'Turn editing mode on' is available for a user // who does not have any other managing capability. $page->set_other_editing_capability('moodle/course:setcurrentsection'); } /** * Generate the section title, wraps it in a link to the section page if page is to be displayed on a separate page. * * @param section_info|stdClass $section The course_section entry from DB * @param stdClass $course The course entry from DB * @return string HTML to output. */ public function section_title($section, $course) { return $this->render(course_get_format($course)->inplace_editable_render_section_name($section)); } /** * Generate the section title to be displayed on the section page, without a link. * * @param section_info|stdClass $section The course_section entry from DB * @param int|stdClass $course The course entry from DB * @return string HTML to output. */ public function section_title_without_link($section, $course) { return $this->render(course_get_format($course)->inplace_editable_render_section_name($section, false)); } } PK$9\֜/KKIformat/topics/classes/output/courseformat/content/section/controlmenu.phpnu[. /** * Contains the default section controls output class. * * @package format_topics * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace format_topics\output\courseformat\content\section; use core\output\action_menu\link as action_menu_link; use core\output\action_menu\link_secondary as action_menu_link_secondary; use core\output\pix_icon; use core_courseformat\output\local\content\section\controlmenu as controlmenu_base; use core\url; /** * Base class to render a course section menu. * * @package format_topics * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class controlmenu extends controlmenu_base { /** @var \core_courseformat\base the course format class */ protected $format; /** @var \section_info the course section class */ protected $section; /** * Generate the edit control items of a section. * * This method must remain public until the final deprecation of section_edit_control_items. * * @return array of edit control items */ public function section_control_items() { $section = $this->section; $parentcontrols = parent::section_control_items(); if ($section->is_orphan() || !$section->sectionnum) { return $parentcontrols; } if (!has_capability('moodle/course:setcurrentsection', $this->coursecontext)) { return $parentcontrols; } return $this->add_control_after($parentcontrols, 'edit', 'highlight', $this->get_section_highlight_item()); } /** * Return the course url. * * @return url */ #[\core\attribute\deprecated( since: '5.0', mdl: 'MDL-82767', reason: 'Not used anymore, use $this->format->get_update_url instead', )] protected function get_course_url(): url { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); $format = $this->format; $section = $this->section; $course = $format->get_course(); $sectionreturn = $format->get_sectionnum(); if ($sectionreturn) { $url = course_get_url($course, $section->sectionnum); } else { $url = course_get_url($course); } $url->param('sesskey', sesskey()); return $url; } /** * Retrieves the view item for the section control menu. * * @return action_menu_link|null The menu item if applicable, otherwise null. */ protected function get_section_highlight_item(): action_menu_link_secondary { $format = $this->format; $section = $this->section; $course = $format->get_course(); $sectionreturn = $format->get_sectionnum(); $highlightoff = get_string('highlightoff'); $highlightofficon = 'i/marked'; $highlighton = get_string('highlight'); $highlightonicon = 'i/marker'; if ($course->marker == $section->sectionnum) { // Show the "light globe" on/off. $action = 'section_unhighlight'; $icon = $highlightofficon; $name = $highlightoff; $attributes = [ 'class' => 'editing_highlight', 'data-action' => 'sectionUnhighlight', 'data-sectionreturn' => $sectionreturn, 'data-id' => $section->id, 'data-icon' => $highlightofficon, 'data-swapname' => $highlighton, 'data-swapicon' => $highlightonicon, ]; } else { $action = 'section_highlight'; $icon = $highlightonicon; $name = $highlighton; $attributes = [ 'class' => 'editing_highlight', 'data-action' => 'sectionHighlight', 'data-sectionreturn' => $sectionreturn, 'data-id' => $section->id, 'data-icon' => $highlightonicon, 'data-swapname' => $highlightoff, 'data-swapicon' => $highlightofficon, ]; } $url = $this->format->get_update_url( action: $action, ids: [$section->id], returnurl: $this->baseurl, ); return new action_menu_link_secondary( url: $url, icon: new pix_icon($icon, ''), text: $name, attributes: $attributes, ); } /** * Return the specific section highlight action. * * @deprecated since Moodle 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return array the action element. */ #[\core\attribute\deprecated( replacement: 'get_section_highlight_item', since: '5.0', mdl: 'MDL-83527', reason: 'Wrong return type', )] protected function get_highlight_control(): array { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); $format = $this->format; $section = $this->section; $course = $format->get_course(); $sectionreturn = $format->get_sectionnum(); $url = $this->get_course_url(); if (!is_null($sectionreturn)) { $url->param('sectionid', $format->get_sectionid()); } $highlightoff = get_string('highlightoff'); $highlightofficon = 'i/marked'; $highlighton = get_string('highlight'); $highlightonicon = 'i/marker'; if ($course->marker == $section->section) { // Show the "light globe" on/off. $url->param('marker', 0); $result = [ 'url' => $url, 'icon' => $highlightofficon, 'name' => $highlightoff, 'pixattr' => ['class' => ''], 'attr' => [ 'class' => 'editing_highlight', 'data-action' => 'sectionUnhighlight', 'data-sectionreturn' => $sectionreturn, 'data-id' => $section->id, 'data-icon' => $highlightofficon, 'data-swapname' => $highlighton, 'data-swapicon' => $highlightonicon, ], ]; } else { $url->param('marker', $section->section); $result = [ 'url' => $url, 'icon' => $highlightonicon, 'name' => $highlighton, 'pixattr' => ['class' => ''], 'attr' => [ 'class' => 'editing_highlight', 'data-action' => 'sectionHighlight', 'data-sectionreturn' => $sectionreturn, 'data-id' => $section->id, 'data-icon' => $highlightonicon, 'data-swapname' => $highlightoff, 'data-swapicon' => $highlightofficon, ], ]; } return $result; } } PK$9\x.=format/topics/classes/output/courseformat/content/section.phpnu[. /** * Contains the default section controls output class. * * @package format_topics * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace format_topics\output\courseformat\content; use core_courseformat\base as course_format; use core_courseformat\output\local\content\section as section_base; use stdClass; /** * Base class to render a course section. * * @package format_topics * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class section extends section_base { /** @var course_format the course format */ protected $format; public function export_for_template(\renderer_base $output): stdClass { $format = $this->format; $data = parent::export_for_template($output); if (!$this->format->get_sectionnum() && !$this->section->get_component_instance()) { $addsectionclass = $format->get_output_classname('content\\addsection'); $addsection = new $addsectionclass($format); $data->numsections = $addsection->export_for_template($output); $data->insertafter = true; } return $data; } } PK$9\5format/topics/classes/output/courseformat/content.phpnu[. /** * Contains the default content output class. * * @package format_topics * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace format_topics\output\courseformat; use core_courseformat\output\local\content as content_base; use renderer_base; /** * Base class to render a course content. * * @package format_topics * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class content extends content_base { /** * @var bool Topic format has also add section after each topic. */ protected $hasaddsection = true; /** * Export this data so it can be used as the context for a mustache template (core/inplace_editable). * * @param renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(renderer_base $output) { global $PAGE; $PAGE->requires->js_call_amd('format_topics/mutations', 'init'); $PAGE->requires->js_call_amd('format_topics/section', 'init'); return parent::export_for_template($output); } } PK$9\a M3format/topics/classes/courseformat/stateactions.phpnu[. namespace format_topics\courseformat; use core_courseformat\stateupdates; use core_courseformat\stateactions as stateactions_base; use core\event\course_module_updated; use cm_info; use section_info; use stdClass; use course_modinfo; use moodle_exception; use context_module; use context_course; /** * Contains the core course state actions specific to topics format. * * @package format_topics * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class stateactions extends stateactions_base { /** * Highlight course section. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids section ids (only ther first one will be highlighted) * @param int $targetsectionid not used * @param int $targetcmid not used */ public function section_highlight( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { global $DB; $this->validate_sections($course, $ids, __FUNCTION__); $coursecontext = context_course::instance($course->id); require_capability('moodle/course:setcurrentsection', $coursecontext); // Get the previous marked section. $modinfo = get_fast_modinfo($course); $previousmarker = $DB->get_field("course", "marker", ['id' => $course->id]); $section = $modinfo->get_section_info_by_id(reset($ids), MUST_EXIST); if ($section->section == $previousmarker) { return; } // Mark the new one. course_set_marker($course->id, $section->section); $updates->add_section_put($section->id); if ($previousmarker) { $section = $modinfo->get_section_info($previousmarker); $updates->add_section_put($section->id); } } /** * Remove highlight from a course sections. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids optional extra section ids to refresh * @param int $targetsectionid not used * @param int $targetcmid not used */ public function section_unhighlight( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { global $DB; $this->validate_sections($course, $ids, __FUNCTION__); $coursecontext = context_course::instance($course->id); require_capability('moodle/course:setcurrentsection', $coursecontext); $affectedsections = []; // Get the previous marked section and unmark it. $modinfo = get_fast_modinfo($course); $previousmarker = $DB->get_field("course", "marker", ['id' => $course->id]); course_set_marker($course->id, 0); $section = $modinfo->get_section_info($previousmarker, MUST_EXIST); $updates->add_section_put($section->id); foreach ($ids as $sectionid) { $section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST); if ($section->section != $previousmarker) { $updates->add_section_put($section->id); } } } } PK$9\y@"format/topics/amd/src/mutations.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 . /** * Format topics mutations. * * An instance of this class will be used to add custom mutations to the course editor. * To make sure the addMutations method find the proper functions, all functions must * be declared as class attributes, not a simple methods. The reason is because many * plugins can add extra mutations to the course editor. * * @module format_topics/mutations * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {getCurrentCourseEditor} from 'core_courseformat/courseeditor'; import DefaultMutations from 'core_courseformat/local/courseeditor/mutations'; import CourseActions from 'core_courseformat/local/content/actions'; class TopicsMutations extends DefaultMutations { /** * Highlight sections. * * It is important to note this mutation method is declared as a class attribute, * See the class jsdoc for more details on why. * * @param {StateManager} stateManager the current state manager * @param {array} sectionIds the list of section ids */ sectionHighlight = async function(stateManager, sectionIds) { const logEntry = this._getLoggerEntry( stateManager, 'section_highlight', sectionIds, {component: 'format_topics'} ); const course = stateManager.get('course'); this.sectionLock(stateManager, sectionIds, true); const updates = await this._callEditWebservice('section_highlight', course.id, sectionIds); stateManager.processUpdates(updates); this.sectionLock(stateManager, sectionIds, false); stateManager.addLoggerEntry(await logEntry); }; /** * Unhighlight sections. * * It is important to note this mutation method is declared as a class attribute, * See the class jsdoc for more details on why. * * @param {StateManager} stateManager the current state manager * @param {array} sectionIds the list of section ids */ sectionUnhighlight = async function(stateManager, sectionIds) { const logEntry = this._getLoggerEntry( stateManager, 'section_unhighlight', sectionIds, {component: 'format_topics'} ); const course = stateManager.get('course'); this.sectionLock(stateManager, sectionIds, true); const updates = await this._callEditWebservice('section_unhighlight', course.id, sectionIds); stateManager.processUpdates(updates); this.sectionLock(stateManager, sectionIds, false); stateManager.addLoggerEntry(await logEntry); }; } export const init = () => { const courseEditor = getCurrentCourseEditor(); // Some plugin (activity or block) may have their own mutations already registered. // This is why we use addMutations instead of setMutations here. courseEditor.addMutations(new TopicsMutations()); // Add direct mutation content actions. CourseActions.addActions({ sectionHighlight: 'sectionHighlight', sectionUnhighlight: 'sectionUnhighlight', }); }; PK$9\*% format/topics/amd/src/section.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 . /** * Format topics section extra logic component. * * @module format_topics/section * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {BaseComponent} from 'core/reactive'; import {getCurrentCourseEditor} from 'core_courseformat/courseeditor'; import Templates from 'core/templates'; class HighlightSection extends BaseComponent { /** * Constructor hook. */ create() { // Optional component name for debugging. this.name = 'format_topics_section'; // Default query selectors. this.selectors = { SETMARKER: `[data-action="sectionHighlight"]`, REMOVEMARKER: `[data-action="sectionUnhighlight"]`, ACTIONTEXT: `.menu-action-text`, ICON: `.icon`, }; // Default classes to toggle on refresh. this.classes = { HIDE: 'd-none', }; // The topics format section specific actions. this.formatActions = { HIGHLIGHT: 'sectionHighlight', UNHIGHLIGHT: 'sectionUnhighlight', }; } /** * Component watchers. * * @returns {Array} of watchers */ getWatchers() { return [ {watch: `section.current:updated`, handler: this._refreshHighlight}, ]; } /** * Update a content section using the state information. * * @param {object} param * @param {Object} param.element details the update details. */ async _refreshHighlight({element}) { let selector; let newAction; if (element.current) { selector = this.selectors.SETMARKER; newAction = this.formatActions.UNHIGHLIGHT; } else { selector = this.selectors.REMOVEMARKER; newAction = this.formatActions.HIGHLIGHT; } // Find the affected action. const affectedAction = this.getElement(`${selector}`, element.id); if (!affectedAction) { return; } // Change action, text and icon. affectedAction.dataset.action = newAction; const actionText = affectedAction.querySelector(this.selectors.ACTIONTEXT); if (affectedAction.dataset?.swapname && actionText) { const oldText = actionText?.innerText; actionText.innerText = affectedAction.dataset.swapname; affectedAction.dataset.swapname = oldText; } const icon = affectedAction.querySelector(this.selectors.ICON); if (affectedAction.dataset?.swapicon && icon) { const newIcon = affectedAction.dataset.swapicon; if (newIcon) { const pixHtml = await Templates.renderPix(newIcon, 'core'); Templates.replaceNode(icon, pixHtml, ''); affectedAction.dataset.swapicon = affectedAction.dataset.icon; affectedAction.dataset.icon = newIcon; } } } } export const init = () => { // Add component to the section. const courseEditor = getCurrentCourseEditor(); if (courseEditor.supportComponents && courseEditor.isEditing) { new HighlightSection({ element: document.getElementById('page'), reactive: courseEditor, }); } }; PK$9\3v,,,format/topics/amd/build/mutations.min.js.mapnu[{"version":3,"file":"mutations.min.js","sources":["../src/mutations.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 * Format topics mutations.\n *\n * An instance of this class will be used to add custom mutations to the course editor.\n * To make sure the addMutations method find the proper functions, all functions must\n * be declared as class attributes, not a simple methods. The reason is because many\n * plugins can add extra mutations to the course editor.\n *\n * @module format_topics/mutations\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport DefaultMutations from 'core_courseformat/local/courseeditor/mutations';\nimport CourseActions from 'core_courseformat/local/content/actions';\n\nclass TopicsMutations extends DefaultMutations {\n\n /**\n * Highlight sections.\n *\n * It is important to note this mutation method is declared as a class attribute,\n * See the class jsdoc for more details on why.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n */\n sectionHighlight = async function(stateManager, sectionIds) {\n const logEntry = this._getLoggerEntry(\n stateManager,\n 'section_highlight',\n sectionIds,\n {component: 'format_topics'}\n );\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice('section_highlight', course.id, sectionIds);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n stateManager.addLoggerEntry(await logEntry);\n\n };\n\n /**\n * Unhighlight sections.\n *\n * It is important to note this mutation method is declared as a class attribute,\n * See the class jsdoc for more details on why.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n */\n sectionUnhighlight = async function(stateManager, sectionIds) {\n const logEntry = this._getLoggerEntry(\n stateManager,\n 'section_unhighlight',\n sectionIds,\n {component: 'format_topics'}\n );\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice('section_unhighlight', course.id, sectionIds);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n stateManager.addLoggerEntry(await logEntry);\n };\n}\n\nexport const init = () => {\n const courseEditor = getCurrentCourseEditor();\n // Some plugin (activity or block) may have their own mutations already registered.\n // This is why we use addMutations instead of setMutations here.\n courseEditor.addMutations(new TopicsMutations());\n // Add direct mutation content actions.\n CourseActions.addActions({\n sectionHighlight: 'sectionHighlight',\n sectionUnhighlight: 'sectionUnhighlight',\n });\n};\n"],"names":["TopicsMutations","DefaultMutations","async","stateManager","sectionIds","logEntry","this","_getLoggerEntry","component","course","get","sectionLock","updates","_callEditWebservice","id","processUpdates","addLoggerEntry","addMutations","addActions","sectionHighlight","sectionUnhighlight"],"mappings":"goBAgCMA,wBAAwBC,8FAWPC,eAAeC,aAAcC,kBACtCC,SAAWC,KAAKC,gBAClBJ,aACA,oBACAC,WACA,CAACI,UAAW,kBAEVC,OAASN,aAAaO,IAAI,eAC3BC,YAAYR,aAAcC,YAAY,SACrCQ,cAAgBN,KAAKO,oBAAoB,oBAAqBJ,OAAOK,GAAIV,YAC/ED,aAAaY,eAAeH,cACvBD,YAAYR,aAAcC,YAAY,GAC3CD,aAAaa,qBAAqBX,wDAajBH,eAAeC,aAAcC,kBACxCC,SAAWC,KAAKC,gBAClBJ,aACA,sBACAC,WACA,CAACI,UAAW,kBAEVC,OAASN,aAAaO,IAAI,eAC3BC,YAAYR,aAAcC,YAAY,SACrCQ,cAAgBN,KAAKO,oBAAoB,sBAAuBJ,OAAOK,GAAIV,YACjFD,aAAaY,eAAeH,cACvBD,YAAYR,aAAcC,YAAY,GAC3CD,aAAaa,qBAAqBX,4BAItB,MACK,0CAGRY,aAAa,IAAIjB,kCAEhBkB,WAAW,CACrBC,iBAAkB,mBAClBC,mBAAoB"}PK$9\ {i==*format/topics/amd/build/section.min.js.mapnu[{"version":3,"file":"section.min.js","sources":["../src/section.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 * Format topics section extra logic component.\n *\n * @module format_topics/section\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport Templates from 'core/templates';\n\nclass HighlightSection extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'format_topics_section';\n // Default query selectors.\n this.selectors = {\n SETMARKER: `[data-action=\"sectionHighlight\"]`,\n REMOVEMARKER: `[data-action=\"sectionUnhighlight\"]`,\n ACTIONTEXT: `.menu-action-text`,\n ICON: `.icon`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n HIDE: 'd-none',\n };\n // The topics format section specific actions.\n this.formatActions = {\n HIGHLIGHT: 'sectionHighlight',\n UNHIGHLIGHT: 'sectionUnhighlight',\n };\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `section.current:updated`, handler: this._refreshHighlight},\n ];\n }\n\n /**\n * Update a content section using the state information.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n async _refreshHighlight({element}) {\n let selector;\n let newAction;\n if (element.current) {\n selector = this.selectors.SETMARKER;\n newAction = this.formatActions.UNHIGHLIGHT;\n } else {\n selector = this.selectors.REMOVEMARKER;\n newAction = this.formatActions.HIGHLIGHT;\n }\n // Find the affected action.\n const affectedAction = this.getElement(`${selector}`, element.id);\n if (!affectedAction) {\n return;\n }\n // Change action, text and icon.\n affectedAction.dataset.action = newAction;\n const actionText = affectedAction.querySelector(this.selectors.ACTIONTEXT);\n if (affectedAction.dataset?.swapname && actionText) {\n const oldText = actionText?.innerText;\n actionText.innerText = affectedAction.dataset.swapname;\n affectedAction.dataset.swapname = oldText;\n }\n const icon = affectedAction.querySelector(this.selectors.ICON);\n if (affectedAction.dataset?.swapicon && icon) {\n const newIcon = affectedAction.dataset.swapicon;\n if (newIcon) {\n const pixHtml = await Templates.renderPix(newIcon, 'core');\n Templates.replaceNode(icon, pixHtml, '');\n affectedAction.dataset.swapicon = affectedAction.dataset.icon;\n affectedAction.dataset.icon = newIcon;\n }\n }\n }\n}\n\nexport const init = () => {\n // Add component to the section.\n const courseEditor = getCurrentCourseEditor();\n if (courseEditor.supportComponents && courseEditor.isEditing) {\n new HighlightSection({\n element: document.getElementById('page'),\n reactive: courseEditor,\n });\n }\n};\n"],"names":["HighlightSection","BaseComponent","create","name","selectors","SETMARKER","REMOVEMARKER","ACTIONTEXT","ICON","classes","HIDE","formatActions","HIGHLIGHT","UNHIGHLIGHT","getWatchers","watch","handler","this","_refreshHighlight","selector","newAction","element","current","affectedAction","getElement","id","dataset","action","actionText","querySelector","swapname","oldText","innerText","icon","swapicon","newIcon","pixHtml","Templates","renderPix","replaceNode","courseEditor","supportComponents","isEditing","document","getElementById","reactive"],"mappings":";;;;;;;sJA2BMA,yBAAyBC,wBAK3BC,cAESC,KAAO,6BAEPC,UAAY,CACbC,6CACAC,kDACAC,+BACAC,mBAGCC,QAAU,CACXC,KAAM,eAGLC,cAAgB,CACjBC,UAAW,mBACXC,YAAa,sBASrBC,oBACW,CACH,CAACC,gCAAkCC,QAASC,KAAKC,uGAWjDC,SACAC,WAFgBC,QAACA,cAGjBA,QAAQC,SACRH,SAAWF,KAAKb,UAAUC,UAC1Be,UAAYH,KAAKN,cAAcE,cAE/BM,SAAWF,KAAKb,UAAUE,aAC1Bc,UAAYH,KAAKN,cAAcC,iBAG7BW,eAAiBN,KAAKO,qBAAcL,UAAYE,QAAQI,QACzDF,sBAILA,eAAeG,QAAQC,OAASP,gBAC1BQ,WAAaL,eAAeM,cAAcZ,KAAKb,UAAUG,6CAC3DgB,eAAeG,gEAASI,UAAYF,WAAY,OAC1CG,QAAUH,MAAAA,kBAAAA,WAAYI,UAC5BJ,WAAWI,UAAYT,eAAeG,QAAQI,SAC9CP,eAAeG,QAAQI,SAAWC,cAEhCE,KAAOV,eAAeM,cAAcZ,KAAKb,UAAUI,wCACrDe,eAAeG,kEAASQ,UAAYD,KAAM,OACpCE,QAAUZ,eAAeG,QAAQQ,YACnCC,QAAS,OACHC,cAAgBC,mBAAUC,UAAUH,QAAS,2BACzCI,YAAYN,KAAMG,QAAS,IACrCb,eAAeG,QAAQQ,SAAWX,eAAeG,QAAQO,KACzDV,eAAeG,QAAQO,KAAOE,yBAM1B,WAEVK,cAAe,0CACjBA,aAAaC,mBAAqBD,aAAaE,eAC3C1C,iBAAiB,CACjBqB,QAASsB,SAASC,eAAe,QACjCC,SAAUL"}PK$9\*(format/topics/amd/build/mutations.min.jsnu[define("format_topics/mutations",["exports","core_courseformat/courseeditor","core_courseformat/local/courseeditor/mutations","core_courseformat/local/content/actions"],(function(_exports,_courseeditor,_mutations,_actions){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.init=void 0,_mutations=_interopRequireDefault(_mutations),_actions=_interopRequireDefault(_actions);class TopicsMutations extends _mutations.default{constructor(){super(...arguments),_defineProperty(this,"sectionHighlight",(async function(stateManager,sectionIds){const logEntry=this._getLoggerEntry(stateManager,"section_highlight",sectionIds,{component:"format_topics"}),course=stateManager.get("course");this.sectionLock(stateManager,sectionIds,!0);const updates=await this._callEditWebservice("section_highlight",course.id,sectionIds);stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1),stateManager.addLoggerEntry(await logEntry)})),_defineProperty(this,"sectionUnhighlight",(async function(stateManager,sectionIds){const logEntry=this._getLoggerEntry(stateManager,"section_unhighlight",sectionIds,{component:"format_topics"}),course=stateManager.get("course");this.sectionLock(stateManager,sectionIds,!0);const updates=await this._callEditWebservice("section_unhighlight",course.id,sectionIds);stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1),stateManager.addLoggerEntry(await logEntry)}))}}_exports.init=()=>{(0,_courseeditor.getCurrentCourseEditor)().addMutations(new TopicsMutations),_actions.default.addActions({sectionHighlight:"sectionHighlight",sectionUnhighlight:"sectionUnhighlight"})}})); //# sourceMappingURL=mutations.min.js.mapPK$9\#> &format/topics/amd/build/section.min.jsnu[define("format_topics/section",["exports","core/reactive","core_courseformat/courseeditor","core/templates"],(function(_exports,_reactive,_courseeditor,_templates){var obj; /** * Format topics section extra logic component. * * @module format_topics/section * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};class HighlightSection extends _reactive.BaseComponent{create(){this.name="format_topics_section",this.selectors={SETMARKER:'[data-action="sectionHighlight"]',REMOVEMARKER:'[data-action="sectionUnhighlight"]',ACTIONTEXT:".menu-action-text",ICON:".icon"},this.classes={HIDE:"d-none"},this.formatActions={HIGHLIGHT:"sectionHighlight",UNHIGHLIGHT:"sectionUnhighlight"}}getWatchers(){return[{watch:"section.current:updated",handler:this._refreshHighlight}]}async _refreshHighlight(_ref){var _affectedAction$datas,_affectedAction$datas2;let selector,newAction,{element:element}=_ref;element.current?(selector=this.selectors.SETMARKER,newAction=this.formatActions.UNHIGHLIGHT):(selector=this.selectors.REMOVEMARKER,newAction=this.formatActions.HIGHLIGHT);const affectedAction=this.getElement("".concat(selector),element.id);if(!affectedAction)return;affectedAction.dataset.action=newAction;const actionText=affectedAction.querySelector(this.selectors.ACTIONTEXT);if(null!==(_affectedAction$datas=affectedAction.dataset)&&void 0!==_affectedAction$datas&&_affectedAction$datas.swapname&&actionText){const oldText=null==actionText?void 0:actionText.innerText;actionText.innerText=affectedAction.dataset.swapname,affectedAction.dataset.swapname=oldText}const icon=affectedAction.querySelector(this.selectors.ICON);if(null!==(_affectedAction$datas2=affectedAction.dataset)&&void 0!==_affectedAction$datas2&&_affectedAction$datas2.swapicon&&icon){const newIcon=affectedAction.dataset.swapicon;if(newIcon){const pixHtml=await _templates.default.renderPix(newIcon,"core");_templates.default.replaceNode(icon,pixHtml,""),affectedAction.dataset.swapicon=affectedAction.dataset.icon,affectedAction.dataset.icon=newIcon}}}}_exports.init=()=>{const courseEditor=(0,_courseeditor.getCurrentCourseEditor)();courseEditor.supportComponents&&courseEditor.isEditing&&new HighlightSection({element:document.getElementById("page"),reactive:courseEditor})}})); //# sourceMappingURL=section.min.js.mapPK$9\d;;format/topics/UPGRADING.mdnu[# format_topics Upgrade notes ## 5.0 ### Deprecated - In format topics, the section controlmenu class deprecates the get_course_url method. This may affect formats extending the topics format and adding extra items to the section menu. Use $this->format->get_update_url instead. For more information see [MDL-82767](https://tracker.moodle.org/browse/MDL-82767) - The get_highlight_control in the section controlmenu class is now deprecated. Use get_section_highlight_item instead For more information see [MDL-83527](https://tracker.moodle.org/browse/MDL-83527) PK$9\L)V-V-*format/topics/tests/format_topics_test.phpnu[. namespace format_topics; use core_external\external_api; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/course/lib.php'); /** * Topics course format related unit tests. * * @package format_topics * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \format_topics */ final class format_topics_test extends \advanced_testcase { /** * Tests for format_topics::get_section_name method with default section names. * * @return void */ public function test_get_section_name(): void { global $DB; $this->resetAfterTest(true); // Generate a course with 5 sections. $generator = $this->getDataGenerator(); $numsections = 5; $course = $generator->create_course(['numsections' => $numsections, 'format' => 'topics'], ['createsections' => true]); // Get section names for course. $coursesections = $DB->get_records('course_sections', ['course' => $course->id]); // Test get_section_name with default section names. $courseformat = course_get_format($course); foreach ($coursesections as $section) { // Assert that with unmodified section names, get_section_name returns the same result as get_default_section_name. $this->assertEquals($courseformat->get_default_section_name($section), $courseformat->get_section_name($section)); } } /** * Tests for format_topics::get_section_name method with modified section names. * * @return void */ public function test_get_section_name_customised(): void { global $DB; $this->resetAfterTest(true); // Generate a course with 5 sections. $generator = $this->getDataGenerator(); $numsections = 5; $course = $generator->create_course(['numsections' => $numsections, 'format' => 'topics'], ['createsections' => true]); // Get section names for course. $coursesections = $DB->get_records('course_sections', ['course' => $course->id]); // Modify section names. $customname = "Custom Section"; foreach ($coursesections as $section) { $section->name = "$customname $section->section"; $DB->update_record('course_sections', $section); } // Requery updated section names then test get_section_name. $coursesections = $DB->get_records('course_sections', ['course' => $course->id]); $courseformat = course_get_format($course); foreach ($coursesections as $section) { // Assert that with modified section names, get_section_name returns the modified section name. $this->assertEquals($section->name, $courseformat->get_section_name($section)); } } /** * Tests for format_topics::get_default_section_name. * * @return void */ public function test_get_default_section_name(): void { global $DB; $this->resetAfterTest(true); // Generate a course with 5 sections. $generator = $this->getDataGenerator(); $numsections = 5; $course = $generator->create_course(['numsections' => $numsections, 'format' => 'topics'], ['createsections' => true]); // Get section names for course. $coursesections = $DB->get_records('course_sections', ['course' => $course->id]); // Test get_default_section_name with default section names. $courseformat = course_get_format($course); foreach ($coursesections as $section) { if ($section->section == 0) { $sectionname = get_string('section0name', 'format_topics'); $this->assertEquals($sectionname, $courseformat->get_default_section_name($section)); } else { $sectionname = get_string('newsection', 'format_topics'); $this->assertEquals($sectionname, $courseformat->get_default_section_name($section)); } } } /** * Test web service updating section name. * * @return void */ public function test_update_inplace_editable(): void { global $CFG, $DB, $PAGE; require_once($CFG->dirroot . '/lib/external/externallib.php'); $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $course = $this->getDataGenerator()->create_course(['numsections' => 5, 'format' => 'topics'], ['createsections' => true]); $section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => 2]); // Call webservice without necessary permissions. try { \core_external::update_inplace_editable('format_topics', 'sectionname', $section->id, 'New section name'); $this->fail('Exception expected'); } catch (\moodle_exception $e) { $this->assertEquals('Course or activity not accessible. (Not enrolled)', $e->getMessage()); } // Change to teacher and make sure that section name can be updated using web service update_inplace_editable(). $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']); $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id); $res = \core_external::update_inplace_editable('format_topics', 'sectionname', $section->id, 'New section name'); $res = external_api::clean_returnvalue(\core_external::update_inplace_editable_returns(), $res); $this->assertEquals('New section name', $res['value']); $this->assertEquals('New section name', $DB->get_field('course_sections', 'name', ['id' => $section->id])); } /** * Test callback updating section name. * * @return void */ public function test_inplace_editable(): void { global $DB, $PAGE; $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(['numsections' => 5, 'format' => 'topics'], ['createsections' => true]); $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']); $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id); $this->setUser($user); $section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => 2]); // Call callback format_topics_inplace_editable() directly. $tmpl = component_callback('format_topics', 'inplace_editable', ['sectionname', $section->id, 'Rename me again']); $this->assertInstanceOf('core\output\inplace_editable', $tmpl); $res = $tmpl->export_for_template($PAGE->get_renderer('core')); $this->assertEquals('Rename me again', $res['value']); $this->assertEquals('Rename me again', $DB->get_field('course_sections', 'name', ['id' => $section->id])); // Try updating using callback from mismatching course format. try { component_callback('format_weeks', 'inplace_editable', ['sectionname', $section->id, 'New name']); $this->fail('Exception expected'); } catch (\moodle_exception $e) { $this->assertEquals(1, preg_match('/^Can\'t find data record in database/', $e->getMessage())); } } /** * Test get_default_course_enddate. * * @return void */ public function test_default_course_enddate(): void { global $CFG, $DB; $this->resetAfterTest(true); require_once($CFG->dirroot . '/course/tests/fixtures/testable_course_edit_form.php'); $this->setTimezone('UTC'); $params = ['format' => 'topics', 'numsections' => 5, 'startdate' => 1445644800]; $course = $this->getDataGenerator()->create_course($params); $category = $DB->get_record('course_categories', ['id' => $course->category]); $args = [ 'course' => $course, 'category' => $category, 'editoroptions' => [ 'context' => \context_course::instance($course->id), 'subdirs' => 0 ], 'returnto' => new \moodle_url('/'), 'returnurl' => new \moodle_url('/'), ]; $courseform = new \testable_course_edit_form(null, $args); $courseform->definition_after_data(); $enddate = $params['startdate'] + get_config('moodlecourse', 'courseduration'); $weeksformat = course_get_format($course->id); $this->assertEquals($enddate, $weeksformat->get_default_course_enddate($courseform->get_quick_form())); } /** * Test for get_view_url(). * * @covers ::get_view_url */ public function test_get_view_url(): void { global $CFG; $this->resetAfterTest(); // Generate a course with two sections (0 and 1) and two modules. $generator = $this->getDataGenerator(); $course1 = $generator->create_course(['format' => 'topics']); course_create_sections_if_missing($course1, [0, 1]); $data = (object)['id' => $course1->id]; $format = course_get_format($course1); $format->update_course_format_options($data); // In page. $this->assertNotEmpty($format->get_view_url(null)); $this->assertNotEmpty($format->get_view_url(0)); $this->assertNotEmpty($format->get_view_url(1)); // Navigation. $this->assertStringContainsString('course/view.php', $format->get_view_url(0)); $this->assertStringContainsString('course/view.php', $format->get_view_url(1)); $this->assertStringContainsString('course/section.php', $format->get_view_url(0, ['navigation' => 1])); $this->assertStringContainsString('course/section.php', $format->get_view_url(1, ['navigation' => 1])); // When sr parameter is defined, the section.php page should be returned. $this->assertStringContainsString('course/section.php', $format->get_view_url(0, ['sr' => 1])); $this->assertStringContainsString('course/section.php', $format->get_view_url(1, ['sr' => 1])); $this->assertStringContainsString('course/section.php', $format->get_view_url(0, ['sr' => 0])); $this->assertStringContainsString('course/section.php', $format->get_view_url(1, ['sr' => 0])); } /** * Test get_required_jsfiles(). * * @covers ::get_required_jsfiles */ public function test_get_required_jsfiles(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(['format' => 'topics']); $format = course_get_format($course); $this->assertEmpty($format->get_required_jsfiles()); } } PK$9\c6format/topics/tests/behat/edit_delete_sections.featurenu[@format @format_topics Feature: Sections can be edited and deleted in custom sections format In order to rearrange my course contents As a teacher I need to edit and delete sections Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | format | coursedisplay | numsections | initsections | | Course 1 | C1 | topics | 0 | 5 | 1 | | Course 2 | C2 | topics | 0 | 1 | 0 | And the following "activities" exist: | activity | name | intro | course | idnumber | section | | assign | Test assignment name | Test assignment description | C1 | assign1 | 0 | | book | Test book name | | C1 | book1 | 1 | | lesson | Test lesson name | Test lesson description | C1 | lesson1 | 4 | | choice | Test choice name | Test choice description | C1 | choice1 | 5 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | And I log in as "teacher1" Scenario: View the default name of the general section in custom sections format Given I am on "Course 1" course homepage with editing mode on When I edit the section "0" Then the field "Section name" matches value "" And I should see "General" Scenario: Edit the default name of the general section in custom sections format Given I am on "Course 1" course homepage with editing mode on And I should see "General" in the "General" "section" When I edit the section "0" and I fill the form with: | Section name | This is the general section | Then I should see "This is the general section" in the "page" "region" Scenario: View the default name of the second section in custom sections format Given I am on "Course 2" course homepage with editing mode on When I edit the section "1" Then the field "Section name" matches value "" And I should see "New section" Scenario: Edit section summary in custom sections format Given I am on "Course 1" course homepage with editing mode on When I edit the section "2" and I fill the form with: | Description | Welcome to section 2 | Then I should see "Welcome to section 2" in the "page" "region" Scenario: Edit section default name in custom sections format Given I am on "Course 1" course homepage with editing mode on When I edit the section "2" and I fill the form with: | Section name | This is the second section | Then I should see "This is the second section" in the "page" "region" And I should not see "Section 2" in the "region-main" "region" @javascript Scenario: Inline edit section name in custom sections format Given I am on "Course 1" course homepage with editing mode on When I set the field "Edit section name" in the "Section 1" "section" to "Midterm evaluation" Then I should not see "Section 1" in the "region-main" "region" And "New name for section" "field" should not exist And I should see "Midterm evaluation" in the "Midterm evaluation" "section" And I am on "Course 1" course homepage And I should not see "Section 1" in the "region-main" "region" And I should see "Midterm evaluation" in the "Midterm evaluation" "section" Scenario: Deleting the last section in custom sections format Given I am on "Course 1" course homepage with editing mode on When I delete section "5" Then I should see "This will delete Section 5 and all the activities it contains." And I press "Delete" And I should not see "Section 5" And I should see "Section 4" Scenario: Deleting the middle section in custom sections format Given I am on "Course 1" course homepage with editing mode on When I delete section "4" And I press "Delete" Then I should not see "Section 4" And I should see "Section 5" And I should not see "Test lesson name" And I should see "Test choice name" in the "Section 5" "section" @javascript Scenario: Adding sections at the end of a custom sections format Given I am on "Course 1" course homepage with editing mode on When I click on "Add section" "link" in the "course-addsection" "region" Then I should see "New section" in the "New section" "section" And I should see "Test choice name" in the "Section 5" "section" @javascript Scenario: Adding sections between in custom sections format Given I am on "Course 1" course homepage with editing mode on When I hover over the "Add section" "link" in the "Section 4" "section" And I click on "Add section" "link" in the "Section 4" "section" Then I should see "New section" in the "New section" "section" And I should see "Test choice name" in the "Section 5" "section" And I should not see "Test choice name" in the "New section" "section" @javascript Scenario: Add a section and then add an activity in it Given I am on "Course 1" course homepage with editing mode on When I click on "Add section" "link" in the "course-addsection" "region" And I add an assign activity to course "Course 1" section "6" and I fill the form with: | Assignment name | Very new activity | | Description | Test | Then I should see "Very new activity" in the "New section" "section" @javascript Scenario: Copy section permalink URL to clipboard Given I am on "Course 1" course homepage with editing mode on When I open section "1" edit menu And I click on "Permalink" "link" in the "Section 1" "section" And I click on "Copy to clipboard" "link" in the "Permalink" "dialogue" Then I should see "Text copied to clipboard" PK$9\_pf 4format/topics/tests/behat/highlight_sections.featurenu[@format @format_topics Feature: Sections can be highlighted In order to mark sections As a teacher I need to highlight and unhighlight sections Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course" exists: | fullname | Course 1 | | shortname | C1 | | format | topics | | coursedisplay | 0 | | numsections | 5 | | initsections | 1 | And the following "activities" exist: | activity | name | intro | course | idnumber | section | | assign | Test assignment name | Test assignment description | C1 | assign1 | 0 | | book | Test book name | Test book description | C1 | book1 | 1 | | lesson | Test lesson name | Test lesson description | C1 | lesson1 | 4 | | choice | Test choice name | Test choice description | C1 | choice1 | 5 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on @javascript Scenario: Highlight a section When I open section "2" edit menu And I click on "Highlight" "link" in the "Section 2" "section" Then I should see "Highlighted" in the "Section 2" "section" @javascript Scenario: Highlight a section when another section is already highlighted Given I open section "3" edit menu And I click on "Highlight" "link" in the "Section 3" "section" And I should see "Highlighted" in the "Section 3" "section" When I open section "2" edit menu And I click on "Highlight" "link" in the "Section 2" "section" Then I should see "Highlighted" in the "Section 2" "section" And I should not see "Highlighted" in the "Section 3" "section" @javascript Scenario: Unhighlight a section Given I open section "3" edit menu And I click on "Highlight" "link" in the "Section 3" "section" And I should see "Highlighted" in the "Section 3" "section" When I open section "3" edit menu And I click on "Unhighlight" "link" in the "Section 3" "section" Then I should not see "Highlighted" in the "Section 3" "section" Scenario: Highlight and unhighlight a section can be done without ajax # Without javascript hidden elements cannot be detected with a simple I should see step. Given ".section.current" "css_element" should not exist When I click on "Highlight" "link" in the "Section 2" "core_courseformat > Section actions menu" Then ".section.current[data-number='2']" "css_element" should exist And I click on "Unhighlight" "link" in the "Section 2" "core_courseformat > Section actions menu" And ".section.current" "css_element" should not exist PK$9\q-//6format/topics/tests/courseformat/stateactions_test.phpnu[. namespace format_topics\courseformat; use core_courseformat\stateupdates; use moodle_exception; use stdClass; /** * Topics course format related unit tests. * * @package format_topics * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class stateactions_test extends \advanced_testcase { /** * Enrol a user into a course and login as this user. * * @param stdClass $course the course object * @param string $rolename the rolename */ private function enrol_user(stdClass $course, string $rolename): void { // Create and enrol user using given role. if ($rolename == 'admin') { $this->setAdminUser(); } else { $user = $this->getDataGenerator()->create_user(); if ($rolename != 'unenroled') { $this->getDataGenerator()->enrol_user($user->id, $course->id, $rolename); } $this->setUser($user); } } /** * Tests for section_highlight method. * * @dataProvider basic_role_provider * @covers ::section_highlight * @param string $rolename The role of the user that will execute the method. * @param bool $expectedexception If this call will raise an exception. */ public function test_section_highlight(string $rolename, bool $expectedexception = false): void { global $DB; $this->resetAfterTest(true); $generator = $this->getDataGenerator(); $course = $generator->create_course( ['numsections' => 4, 'format' => 'topics'], ['createsections' => true] ); $this->enrol_user($course, $rolename); $sectionrecords = $DB->get_records('course_sections', ['course' => $course->id], 'section'); $sectionids = []; foreach ($sectionrecords as $section) { $sectionids[] = $section->id; } // Initialise stateupdates. $courseformat = course_get_format($course->id); $updates = new stateupdates($courseformat); // All state actions accepts batch editing (an array of sections in this case). However, // only one course section can be marked as highlighted. This means that if we send more // than one section id only the first one will be highlighted and the rest will be ignored. $methodparam = [ $sectionids[1], $sectionids[2], $sectionids[3], ]; // Actions have an array of ids as param but only the first one will be highlighted. $highlightid = reset($methodparam); $highlight = $sectionrecords[$highlightid]; if ($expectedexception) { $this->expectException(moodle_exception::class); } // Execute given method. $actions = new stateactions(); $actions->section_highlight( $updates, $course, $methodparam ); // Check state returned after executing given action. $updatelist = $updates->jsonSerialize(); $this->assertCount(1, $updatelist); $update = reset($updatelist); $this->assertEquals('section', $update->name); $this->assertEquals('put', $update->action); $this->assertEquals($highlightid, $update->fields->id); $this->assertEquals(1, $update->fields->current); // Check DB sections. $this->assertEquals($highlight->section, $DB->get_field("course", "marker", ['id' => $course->id])); } /** * Tests for section_unhighlight method. * * @dataProvider basic_role_provider * @covers ::section_unhighlight * @param string $rolename The role of the user that will execute the method. * @param bool $expectedexception If this call will raise an exception. */ public function test_section_unhighlight(string $rolename, bool $expectedexception = false): void { global $DB; $this->resetAfterTest(true); $generator = $this->getDataGenerator(); $course = $generator->create_course( ['numsections' => 4, 'format' => 'topics'], ['createsections' => true] ); // Highlight section 1. course_set_marker($course->id, 1); $this->enrol_user($course, $rolename); $sectionrecords = $DB->get_records('course_sections', ['course' => $course->id], 'section'); $sectionids = []; foreach ($sectionrecords as $section) { $sectionids[] = $section->id; } // Initialise stateupdates. $courseformat = course_get_format($course->id); $updates = new stateupdates($courseformat); // The section_unhighlight accepts extra sections to refresh the state data. $methodparam = [ $sectionids[3], $sectionids[4], ]; if ($expectedexception) { $this->expectException(moodle_exception::class); } // Execute given method. $actions = new stateactions(); $actions->section_unhighlight( $updates, $course, $methodparam ); // The Unhilight mutation always return the previous highlighted // section (1) and all the extra sections passed (3, and 4) to ensure // all of them are updated. $returnedsectionnumbers = [1, 3, 4]; // Check state returned after executing given action. $updatelist = $updates->jsonSerialize(); $this->assertCount(3, $updatelist); foreach ($updatelist as $update) { $this->assertEquals('section', $update->name); $this->assertEquals('put', $update->action); $this->assertContains($update->fields->number, $returnedsectionnumbers); $this->assertEquals(0, $update->fields->current); } // Check DB sections. $this->assertEquals(0, $DB->get_field("course", "marker", ['id' => $course->id])); } /** * Data provider for basic role tests. * * @return array the testing scenarios */ public static function basic_role_provider(): array { return [ 'admin' => [ 'rolename' => 'admin', 'expectedexception' => false, ], 'editingteacher' => [ 'rolename' => 'editingteacher', 'expectedexception' => false, ], 'teacher' => [ 'rolename' => 'teacher', 'expectedexception' => true, ], 'student' => [ 'rolename' => 'student', 'expectedexception' => true, ], 'guest' => [ 'rolename' => 'guest', 'expectedexception' => true, ], 'unenroled' => [ 'rolename' => 'unenroled', 'expectedexception' => true, ], ]; } } PK$9\C&NNformat/topics/format.phpnu[. /** * Topics course format. Display the whole course as "topics" made of modules. * * @package format_topics * @copyright 2006 The Open University * @author N.D.Freear@open.ac.uk, and others. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/filelib.php'); require_once($CFG->libdir.'/completionlib.php'); // Horrible backwards compatible parameter aliasing. if ($topic = optional_param('topic', 0, PARAM_INT)) { $url = $PAGE->url; $url->param('section', $topic); debugging('Outdated topic param passed to course/view.php', DEBUG_DEVELOPER); redirect($url); } // End backwards-compatible aliasing. // Retrieve course format option fields and add them to the $course object. $format = course_get_format($course); $course = $format->get_course(); $context = context_course::instance($course->id); if (($marker >= 0) && has_capability('moodle/course:setcurrentsection', $context) && confirm_sesskey()) { $course->marker = $marker; course_set_marker($course->id, $marker); } // Make sure section 0 is created. course_create_sections_if_missing($course, 0); $renderer = $PAGE->get_renderer('format_topics'); if (!is_null($displaysection)) { $format->set_sectionnum($displaysection); } $outputclass = $format->get_output_classname('content'); $widget = new $outputclass($format); echo $renderer->render($widget); PK$9\~PPformat/topics/settings.phpnu[. /** * Settings for format_topics * * @package format_topics * @copyright 2020 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { $url = new moodle_url('/admin/course/resetindentation.php', ['format' => 'topics']); $link = html_writer::link($url, get_string('resetindentation', 'admin')); $settings->add(new admin_setting_configcheckbox( 'format_topics/indentation', new lang_string('indentation', 'format_topics'), new lang_string('indentation_help', 'format_topics').'
'.$link, 1 )); } PK$9\(o&&format/lib.phpnu[. /** * Base class for course format plugins * * @package core_course * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; use core_courseformat\base as course_format; use core_courseformat\output\site_renderer; /** * Returns an instance of format class (extending course_format) for given course * * @param int|stdClass $courseorid either course id or * an object that has the property 'format' and may contain property 'id' * @return course_format */ function course_get_format($courseorid) { return course_format::instance($courseorid); } /** * Pseudo course format used for the site main page * * @package core_course * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class format_site extends course_format { /** * Returns the display name of the given section that the course prefers. * * @param int|stdClass $section Section object from database or just field section.section * @return string Display name that the course format prefers, e.g. "Topic 2" */ function get_section_name($section) { $section = $this->get_section($section); if ((string)$section->name !== '') { // Return the name the user set. return format_string($section->name, true, array('context' => context_course::instance($this->courseid))); } // The section zero is located in a block. if ($section->sectionnum == 0) { return get_string('block'); } return get_string('site'); } /** * For this fake course referring to the whole site, the site homepage is always returned * regardless of arguments * * @param int|stdClass $section * @param array $options * @return null|moodle_url */ public function get_view_url($section, $options = array()) { return new moodle_url('/', array('redirect' => 0)); } /** * Returns the list of blocks to be automatically added on the site frontpage when moodle is installed * * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT * each of values is an array of block names (for left and right side columns) */ public function get_default_blocks() { return blocks_get_default_site_course_blocks(); } #[\Override] public function supports_ajax() { // All home page is rendered in the backend, we only need an ajax editor components in edit mode. // This will also prevent redirectng to the login page when a guest tries to access the site, // and will make the home page loading faster. $ajaxsupport = new stdClass(); $ajaxsupport->capable = $this->show_editor(); return $ajaxsupport; } #[\Override] public function supports_components() { return true; } #[\Override] public function uses_sections() { return true; } /** * Definitions of the additional options that site uses * * @param bool $foreditform * @return array of options */ public function course_format_options($foreditform = false) { static $courseformatoptions = false; if ($courseformatoptions === false) { $courseformatoptions = array( 'numsections' => array( 'default' => 1, 'type' => PARAM_INT, ), ); } return $courseformatoptions; } /** * Returns whether this course format allows the activity to * have "triple visibility state" - visible always, hidden on course page but available, hidden. * * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module) * @param stdClass|section_info $section section where this module is located or will be added to * @return bool */ public function allow_stealth_module_visibility($cm, $section) { return true; } /** * Returns instance of page renderer used by the site page * * @param moodle_page $page the current page * @return renderer_base */ public function get_renderer(moodle_page $page) { return new site_renderer($page, null); } /** * Site format uses only section 1. * * @return int */ public function get_sectionnum(): int { return 1; } } /** * 'Converts' a value from what is stored in the database into what is used by edit forms. * * @param array $dest The destination array * @param array $source The source array * @param array $option The definition structure of the option. * @param string $optionname The name of the option, as provided in the definition. */ function contract_value(array &$dest, array $source, array $option, string $optionname): void { if (substr($optionname, -7) == '_editor') { // Suffix '_editor' indicates that the element is an editor. $name = substr($optionname, 0, -7); if (isset($source[$name])) { $dest[$optionname] = [ 'text' => clean_param_if_not_null($source[$name], $option['type'] ?? PARAM_RAW), 'format' => clean_param_if_not_null($source[$name . 'format'], PARAM_INT), ]; } } else { if (isset($source[$optionname])) { $dest[$optionname] = clean_param_if_not_null($source[$optionname], $option['type'] ?? PARAM_RAW); } } } /** * Cleans the given param, unless it is null. * * @param mixed $param The variable we are cleaning. * @param string $type Expected format of param after cleaning. * @return mixed Null if $param is null, otherwise the cleaned value. * @throws coding_exception */ function clean_param_if_not_null($param, string $type = PARAM_RAW) { if ($param === null) { return null; } else { return clean_param($param, $type); } } /** * 'Converts' a value from what is used in edit forms into a value(s) to be stored in the database. * * @param array $dest The destination array * @param array $source The source array * @param array $option The definition structure of the option. * @param string $optionname The name of the option, as provided in the definition. */ function expand_value(array &$dest, array $source, array $option, string $optionname): void { if (substr($optionname, -7) == '_editor') { // Suffix '_editor' indicates that the element is an editor. $name = substr($optionname, 0, -7); if (is_string($source[$optionname])) { $dest[$name] = clean_param($source[$optionname], $option['type'] ?? PARAM_RAW); $dest[$name . 'format'] = 1; } else { $dest[$name] = clean_param($source[$optionname]['text'], $option['type'] ?? PARAM_RAW); $dest[$name . 'format'] = clean_param($source[$optionname]['format'], PARAM_INT); } unset($dest[$optionname]); } else { $dest[$optionname] = clean_param($source[$optionname], $option['type'] ?? PARAM_RAW); } } /** * Course-module fragment renderer method. * * The fragment arguments are id and sr (section return). * * @param array $args The fragment arguments. * @return string The rendered cm item. * * @throws require_login_exception */ function core_courseformat_output_fragment_cmitem($args): string { global $PAGE; [$course, $cm] = get_course_and_cm_from_cmid($args['id']); if (!can_access_course($course, null, '', true) || !$cm->uservisible) { throw new require_login_exception('Activity is not available'); } $format = course_get_format($course); if (isset($args['pagesectionid'])) { $format->set_sectionid($args['pagesectionid']); } else if (isset($args['sr'])) { $format->set_sectionnum($args['sr']); } $renderer = $format->get_renderer($PAGE); $section = $cm->get_section_info(); return $renderer->course_section_updated_cm_item($format, $section, $cm); } /** * Section fragment renderer method. * * The fragment arguments are courseid, section id and sr (section return). * * @param array $args The fragment arguments. * @return string The rendered section. * * @throws require_login_exception */ function core_courseformat_output_fragment_section($args): string { global $PAGE; $course = get_course($args['courseid']); if (!can_access_course($course, null, '', true)) { throw new require_login_exception('Course is not available'); } $format = course_get_format($course); if (isset($args['pagesectionid'])) { $format->set_sectionid($args['pagesectionid']); } else if (isset($args['sr'])) { $format->set_sectionnum($args['sr']); } $modinfo = $format->get_modinfo(); $section = $modinfo->get_section_info_by_id($args['id'], MUST_EXIST); if (!$section->uservisible) { throw new require_login_exception('Section is not available'); } $renderer = $format->get_renderer($PAGE); return $renderer->course_section_updated($format, $section); } PK$9\Vw1^9^9format/formatlegacy.phpnu[. /** * Course format class to allow plugins developed for Moodle 2.3 to work in the new API * * @package core_course * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Course format class to allow plugins developed for Moodle 2.3 to work in the new API * * @package core_course * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class format_legacy extends core_courseformat\base { /** * Returns true if this course format uses sections * * This function calls function callback_FORMATNAME_uses_sections() if it exists * * @return bool */ public function uses_sections() { global $CFG; // Note that lib.php in course format folder is already included by now $featurefunction = 'callback_'.$this->format.'_uses_sections'; if (function_exists($featurefunction)) { return $featurefunction(); } return false; } /** * Returns the display name of the given section that the course prefers. * * This function calls function callback_FORMATNAME_get_section_name() if it exists * * @param int|stdClass $section Section object from database or just field section.section * @return string Display name that the course format prefers, e.g. "Topic 2" */ public function get_section_name($section) { // Use course formatter callback if it exists $namingfunction = 'callback_'.$this->format.'_get_section_name'; if (function_exists($namingfunction) && ($course = $this->get_course())) { return $namingfunction($course, $this->get_section($section)); } // else, default behavior: return parent::get_section_name($section); } /** * The URL to use for the specified course (with section) * * This function calls function callback_FORMATNAME_get_section_url() if it exists * * @param int|stdClass $section Section object from database or just field course_sections.section * if omitted the course view page is returned * @param array $options options for view URL. At the moment core uses: * 'navigation' (bool) if true and section has no separate page, the function returns null * 'sr' (int) used by multipage formats to specify to which section to return * @return null|moodle_url */ public function get_view_url($section, $options = array()) { // Use course formatter callback if it exists $featurefunction = 'callback_'.$this->format.'_get_section_url'; if (function_exists($featurefunction) && ($course = $this->get_course())) { if (is_object($section)) { $sectionnum = $section->section; } else { $sectionnum = $section; } if ($sectionnum) { $url = $featurefunction($course, $sectionnum); if ($url || !empty($options['navigation'])) { return $url; } } } // if function is not defined if (!$this->uses_sections() || !array_key_exists('coursedisplay', $this->course_format_options())) { // default behaviour return parent::get_view_url($section, $options); } $course = $this->get_course(); $url = new moodle_url('/course/view.php', array('id' => $course->id)); $sr = null; if (array_key_exists('sr', $options)) { $sr = $options['sr']; } if (is_object($section)) { $sectionno = $section->section; } else { $sectionno = $section; } if ($sectionno !== null) { if ($sr !== null) { if ($sr) { $usercoursedisplay = COURSE_DISPLAY_MULTIPAGE; $sectionno = $sr; } else { $usercoursedisplay = COURSE_DISPLAY_SINGLEPAGE; } } else { $usercoursedisplay = $course->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE; } if ($sectionno != 0 && $usercoursedisplay == COURSE_DISPLAY_MULTIPAGE) { $url->param('section', $sectionno); } else { if (!empty($options['navigation'])) { return null; } $url->set_anchor('section-'.$sectionno); } } return $url; } /** * Returns the information about the ajax support in the given source format * * This function calls function callback_FORMATNAME_ajax_support() if it exists * * The returned object's property (boolean)capable indicates that * the course format supports Moodle course ajax features. * * @return stdClass */ public function supports_ajax() { // set up default values $ajaxsupport = parent::supports_ajax(); // get the information from the course format library $featurefunction = 'callback_'.$this->format.'_ajax_support'; if (function_exists($featurefunction)) { $formatsupport = $featurefunction(); if (isset($formatsupport->capable)) { $ajaxsupport->capable = $formatsupport->capable; } } return $ajaxsupport; } /** * Loads all of the course sections into the navigation * * First this function calls callback_FORMATNAME_display_content() if it exists to check * if the navigation should be extended at all * * Then it calls function callback_FORMATNAME_load_content() if it exists to actually extend * navigation * * By default the parent method is called * * @param global_navigation $navigation * @param navigation_node $node The course node within the navigation */ public function extend_course_navigation($navigation, navigation_node $node) { global $PAGE; // if course format displays section on separate pages and we are on course/view.php page // and the section parameter is specified, make sure this section is expanded in // navigation if ($navigation->includesectionnum === false) { $selectedsection = optional_param('section', null, PARAM_INT); if ($selectedsection !== null && (!defined('AJAX_SCRIPT') || AJAX_SCRIPT == '0') && $PAGE->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) { $navigation->includesectionnum = $selectedsection; } } // check if there are callbacks to extend course navigation $displayfunc = 'callback_'.$this->format.'_display_content'; if (function_exists($displayfunc) && !$displayfunc()) { return; } $featurefunction = 'callback_'.$this->format.'_load_content'; if (function_exists($featurefunction) && ($course = $this->get_course())) { $featurefunction($navigation, $course, $node); } else { parent::extend_course_navigation($navigation, $node); } } /** * Custom action after section has been moved in AJAX mode * * Used in course/rest.php * * This function calls function callback_FORMATNAME_ajax_section_move() if it exists * * @return array This will be passed in ajax respose */ function ajax_section_move() { $featurefunction = 'callback_'.$this->format.'_ajax_section_move'; if (function_exists($featurefunction) && ($course = $this->get_course())) { return $featurefunction($course); } else { return parent::ajax_section_move(); } } /** * Returns the list of blocks to be automatically added for the newly created course * * This function checks the existence of the file config.php in the course format folder. * If file exists and contains the code * $format['defaultblocks'] = 'leftblock1,leftblock2:rightblock1,rightblock2'; * these blocks are used, otherwise parent function is called * * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT * each of values is an array of block names (for left and right side columns) */ public function get_default_blocks() { global $CFG; $formatconfig = $CFG->dirroot.'/course/format/'.$this->format.'/config.php'; $format = array(); // initialize array in external file if (is_readable($formatconfig)) { include($formatconfig); } if (!empty($format['defaultblocks'])) { return blocks_parse_default_blocks_list($format['defaultblocks']); } return parent::get_default_blocks(); } /** * Definitions of the additional options that this course format uses for course * * By default course formats have the options that existed in Moodle 2.3: * - coursedisplay * - numsections * - hiddensections * * @param bool $foreditform * @return array of options */ public function course_format_options($foreditform = false) { static $courseformatoptions = false; if ($courseformatoptions === false) { $courseconfig = get_config('moodlecourse'); $courseformatoptions = array( 'numsections' => array( 'default' => $courseconfig->numsections, 'type' => PARAM_INT, ), 'hiddensections' => array( 'default' => $courseconfig->hiddensections ?? 0, 'type' => PARAM_INT, ), 'coursedisplay' => array( 'default' => $courseconfig->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE, 'type' => PARAM_INT, ), ); } if ($foreditform && !isset($courseformatoptions['coursedisplay']['label'])) { $courseconfig = get_config('moodlecourse'); $sectionmenu = array(); for ($i = 0; $i <= $courseconfig->maxsections; $i++) { $sectionmenu[$i] = "$i"; } $courseformatoptionsedit = array( 'numsections' => array( 'label' => new lang_string('numberweeks'), 'element_type' => 'select', 'element_attributes' => array($sectionmenu), ), 'hiddensections' => array( 'label' => new lang_string('hiddensections'), 'help' => 'hiddensections', 'help_component' => 'moodle', 'element_type' => 'select', 'element_attributes' => array( array( 0 => new lang_string('hiddensectionscollapsed'), 1 => new lang_string('hiddensectionsinvisible') ) ), ), 'coursedisplay' => array( 'label' => new lang_string('coursedisplay'), 'element_type' => 'select', 'element_attributes' => array( array( COURSE_DISPLAY_SINGLEPAGE => new lang_string('coursedisplay_single'), COURSE_DISPLAY_MULTIPAGE => new lang_string('coursedisplay_multi') ) ), 'help' => 'coursedisplay', 'help_component' => 'moodle', ) ); $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit); } return $courseformatoptions; } /** * Updates format options for a course * * Legacy course formats may assume that course format options * ('coursedisplay', 'numsections' and 'hiddensections') are shared between formats. * Therefore we make sure to copy them from the previous format * * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data * @param stdClass $oldcourse if this function is called from {@link update_course()} * this object contains information about the course before update * @return bool whether there were any changes to the options values */ public function update_course_format_options($data, $oldcourse = null) { global $DB; if ($oldcourse !== null) { $data = (array)$data; $oldcourse = (array)$oldcourse; $options = $this->course_format_options(); foreach ($options as $key => $unused) { if (!array_key_exists($key, $data)) { if (array_key_exists($key, $oldcourse)) { $data[$key] = $oldcourse[$key]; } else if ($key === 'numsections') { // If previous format does not have the field 'numsections' and this one does, // and $data['numsections'] is not set fill it with the maximum section number from the DB $maxsection = $DB->get_field_sql('SELECT max(section) from {course_sections} WHERE course = ?', array($this->courseid)); if ($maxsection) { // If there are no sections, or just default 0-section, 'numsections' will be set to default $data['numsections'] = $maxsection; } } } } } return $this->update_format_options($data); } } PK$9\VoAA format/singleactivity/styles.cssnu[.format-singleactivity .tree_item.orphaned a { color: red; } PK$9\nn7format/singleactivity/lang/en/format_singleactivity.phpnu[. /** * Strings for component 'format_singleactivity' * * @package format_singleactivity * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['activitytype'] = 'Type of activity'; $string['activitytype_help'] = 'Choose the type of activity or resource to use in this course'; $string['defactivitytype'] = 'Default type of activity'; $string['defactivitytypedesc'] = 'Specify the activity type that will be selected by default when creating a new course'; $string['erroractivitytype'] = 'Type of activity is not set up in Course settings'; $string['orphaned'] = 'Orphaned'; $string['orphanedwarning'] = 'These activities are unreachable by users!'; $string['pluginname'] = 'Single activity'; $string['plugin_description'] = 'The course contains only one activity or resource.'; $string['sectionname'] = ''; $string['warningchangeformat'] = 'When changing the existing course format to "Single activity" make sure that you removed all extra activities from the course including "Announcements". Note that sections structure may be modified.'; $string['privacy:metadata'] = 'The Single activity format plugin does not store any personal data.'; PK$9\/ϓZZformat/singleactivity/lib.phpnu[. /** * This file contains main class for the course format singleactivity * * @package format_singleactivity * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_courseformat\sectiondelegate; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot. '/course/format/lib.php'); /** * Main class for the singleactivity course format * * @package format_singleactivity * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class format_singleactivity extends core_courseformat\base { /** @var cm_info the current activity. Use get_activity() to retrieve it. */ private $activity = false; /** @var int The category ID guessed from the form data. */ private $categoryid = false; /** * The URL to use for the specified course * * @param int|stdClass $section Section object from database or just field course_sections.section * if null the course view page is returned * @param array $options options for view URL. At the moment core uses: * 'navigation' (bool) ignored by this format * 'sr' (int) ignored by this format * @return moodle_url */ public function get_view_url($section, $options = array()) { return new moodle_url('/course/view.php', ['id' => $this->courseid]); } /** * Loads all of the course sections into the navigation * * @param global_navigation $navigation * @param navigation_node $node The course node within the navigation */ public function extend_course_navigation($navigation, navigation_node $node) { // Display orphaned activities for the users who can see them. $context = context_course::instance($this->courseid); if (has_capability('moodle/course:viewhiddensections', $context)) { $modinfo = get_fast_modinfo($this->courseid); if (!empty($modinfo->sections[1])) { $section1 = $modinfo->get_section_info(1); // Show orphaned activities. $orphanednode = $node->add(get_string('orphaned', 'format_singleactivity'), $this->get_view_url(1), navigation_node::TYPE_SECTION, null, $section1->id); $orphanednode->nodetype = navigation_node::NODETYPE_BRANCH; $orphanednode->add_class('orphaned'); foreach ($modinfo->sections[1] as $cmid) { if (has_capability('moodle/course:viewhiddenactivities', context_module::instance($cmid))) { $this->navigation_add_activity($orphanednode, $modinfo->cms[$cmid]); } } } } } /** * Adds a course module to the navigation node * * This is basically copied from function global_navigation::load_section_activities() * because it is not accessible from outside. * * @param navigation_node $node * @param cm_info $cm * @return null|navigation_node */ protected function navigation_add_activity(navigation_node $node, $cm) { if (!$cm->uservisible) { return null; } $action = $cm->url; if (!$action) { // Do not add to navigation activity without url (i.e. labels). return null; } $activityname = format_string($cm->name, true, array('context' => context_module::instance($cm->id))); if ($cm->icon) { $icon = new pix_icon($cm->icon, $cm->modfullname, $cm->iconcomponent); } else { $icon = new pix_icon('monologo', $cm->modfullname, $cm->modname); } $activitynode = $node->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon); if (global_navigation::module_extends_navigation($cm->modname)) { $activitynode->nodetype = navigation_node::NODETYPE_BRANCH; } else { $activitynode->nodetype = navigation_node::NODETYPE_LEAF; } return $activitynode; } /** * Returns the list of blocks to be automatically added for the newly created course * * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT * each of values is an array of block names (for left and right side columns) */ public function get_default_blocks() { // No blocks for this format because course view page is not displayed anyway. return array( BLOCK_POS_LEFT => array(), BLOCK_POS_RIGHT => array() ); } /** * Definitions of the additional options that this course format uses for course * * Singleactivity course format uses one option 'activitytype' * * @param bool $foreditform * @return array of options */ public function course_format_options($foreditform = false) { static $courseformatoptions = false; $fetchtypes = $courseformatoptions === false; $fetchtypes = $fetchtypes || ($foreditform && !isset($courseformatoptions['activitytype']['label'])); if ($fetchtypes) { $availabletypes = $this->get_supported_activities(); if ($this->courseid) { // The course exists. Test against the course. $testcontext = context_course::instance($this->courseid); } else if ($this->categoryid) { // The course does not exist yet, but we have a category ID that we can test against. $testcontext = context_coursecat::instance($this->categoryid); } else { // The course does not exist, and we somehow do not have a category. Test capabilities against the system context. $testcontext = context_system::instance(); } foreach (array_keys($availabletypes) as $activity) { $capability = "mod/{$activity}:addinstance"; if (!has_capability($capability, $testcontext)) { if (!$this->categoryid) { unset($availabletypes[$activity]); } else { // We do not have a course yet, so we guess if the user will have the capability to add the activity after // creating the course. $categorycontext = \context_coursecat::instance($this->categoryid); if (!guess_if_creator_will_have_course_capability($capability, $categorycontext)) { unset($availabletypes[$activity]); } } } } } if ($courseformatoptions === false) { $config = get_config('format_singleactivity'); $courseformatoptions = array( 'activitytype' => array( 'default' => $config->activitytype, 'type' => PARAM_TEXT, ), ); if (!empty($availabletypes) && !isset($availabletypes[$config->activitytype])) { $courseformatoptions['activitytype']['default'] = array_keys($availabletypes)[0]; } } if ($foreditform && !isset($courseformatoptions['activitytype']['label'])) { $courseformatoptionsedit = array( 'activitytype' => array( 'label' => new lang_string('activitytype', 'format_singleactivity'), 'help' => 'activitytype', 'help_component' => 'format_singleactivity', 'element_type' => 'select', 'element_attributes' => array($availabletypes), ), ); $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit); } return $courseformatoptions; } /** * Adds format options elements to the course/section edit form * * This function is called from {@link course_edit_form::definition_after_data()} * * Format singleactivity adds a warning when format of the course is about to be changed. * * @param MoodleQuickForm $mform form the elements are added to * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form * @return array array of references to the added form elements */ public function create_edit_form_elements(&$mform, $forsection = false) { global $PAGE; if (!$this->course && $submitvalues = $mform->getSubmitValues()) { $this->categoryid = $submitvalues['category']; } $elements = parent::create_edit_form_elements($mform, $forsection); if (!$forsection && ($course = $PAGE->course) && !empty($course->format) && $course->format !== 'site' && $course->format !== 'singleactivity') { // This is the existing course in other format, display a warning. $element = $mform->addElement('static', '', '', html_writer::tag('span', get_string('warningchangeformat', 'format_singleactivity'), array('class' => 'error'))); array_unshift($elements, $element); } return $elements; } /** * Make sure that current active activity is in section 0 * * All other activities are moved to section 1 that will be displayed as 'Orphaned'. * It may be needed after the course format was changed or activitytype in * course settings has been changed. * * @return null|cm_info current activity */ public function reorder_activities() { course_create_sections_if_missing($this->courseid, array(0, 1)); foreach ($this->get_sections() as $sectionnum => $section) { if (($sectionnum && $section->visible) || (!$sectionnum && !$section->visible)) { // Make sure that 0 section is visible and all others are hidden. set_section_visible($this->courseid, $sectionnum, $sectionnum == 0); } } $modinfo = get_fast_modinfo($this->courseid); // Find the current activity (first activity with the specified type in all course activities). $activitytype = $this->get_activitytype(); $activity = null; if (!empty($activitytype)) { foreach ($modinfo->sections as $sectionnum => $cmlist) { foreach ($cmlist as $cmid) { if ($modinfo->cms[$cmid]->modname === $activitytype) { $activity = $modinfo->cms[$cmid]; break 2; } } } } // Make sure the current activity is in the 0-section. $changed = false; if ($activity && $activity->sectionnum != 0) { moveto_module($activity, $modinfo->get_section_info(0)); $changed = true; } if ($activity && !$activity->visible) { set_coursemodule_visible($activity->id, 1); $changed = true; } if ($changed) { // Cache was reset so get modinfo again. $modinfo = get_fast_modinfo($this->courseid); } // Move all other activities into section 1 (the order must be kept). $hasvisibleactivities = false; $firstorphanedcm = null; foreach ($modinfo->sections as $sectionnum => $cmlist) { if ($sectionnum && !empty($cmlist) && $firstorphanedcm === null) { $firstorphanedcm = reset($cmlist); } foreach ($cmlist as $cmid) { if ($sectionnum > 1) { // These module types cannot be moved from section 0. if (!$modinfo->cms[$cmid]->is_of_type_that_can_display()) { continue; } moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1)); } else if (!$hasvisibleactivities && $sectionnum == 1 && $modinfo->get_cm($cmid)->visible) { $hasvisibleactivities = true; } } } if (!empty($modinfo->sections[0])) { foreach ($modinfo->sections[0] as $cmid) { // These module types cannot be moved from section 0. if (!$modinfo->cms[$cmid]->is_of_type_that_can_display()) { continue; } if (!$activity || $cmid != $activity->id) { moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1), $firstorphanedcm); } } } if ($hasvisibleactivities) { set_section_visible($this->courseid, 1, false); } return $activity; } /** * Returns the name of activity type used for this course * * @return string|null */ protected function get_activitytype() { $options = $this->get_format_options(); $availabletypes = $this->get_supported_activities(); if (!empty($options['activitytype']) && array_key_exists($options['activitytype'], $availabletypes)) { return $options['activitytype']; } else { return null; } } /** * Returns the current activity if exists * * @return null|cm_info */ protected function get_activity() { if ($this->activity === false) { $this->activity = $this->reorder_activities(); } return $this->activity; } /** * Get the activities supported by the format. * * Here we ignore the modules that do not have a page of their own or need sections, * like the label or subsection. * * @return array array($module => $name of the module). */ public static function get_supported_activities() { $availabletypes = get_module_types_names(); foreach ($availabletypes as $module => $name) { if ( plugin_supports('mod', $module, FEATURE_NO_VIEW_LINK, false) || sectiondelegate::has_delegate_class('mod_' . $module) || !course_modinfo::is_mod_type_visible_on_course($module) ) { unset($availabletypes[$module]); } } return $availabletypes; } /** * Checks if the current user can add the activity of the specified type to this course. * * @return bool */ protected function can_add_activity() { global $CFG; if (!($modname = $this->get_activitytype())) { return false; } if (!has_capability('moodle/course:manageactivities', context_course::instance($this->courseid))) { return false; } if (!course_allowed_module($this->get_course(), $modname)) { return false; } $libfile = "$CFG->dirroot/mod/$modname/lib.php"; if (!file_exists($libfile)) { return null; } return true; } /** * Checks if the activity type has multiple items in the activity chooser. * * @return bool|null (null if the check is not possible) */ public function activity_has_subtypes() { global $USER; if (!($modname = $this->get_activitytype())) { return null; } $contentitemservice = \core_course\local\factory\content_item_service_factory::get_content_item_service(); $metadata = $contentitemservice->get_content_items_for_user_in_course($USER, $this->get_course()); // If there are multiple items originating from this mod_xxx component, then it's deemed to have subtypes. // If there is only 1 item, but it's not a reference to the core content item for the module, then it's also deemed to // have subtypes. $count = 0; foreach ($metadata as $key => $moduledata) { if ('mod_'.$modname === $moduledata->componentname) { $count ++; } } if ($count > 1) { return true; } else { // Get the single item. $itemmetadata = $metadata[array_search('mod_' . $modname, array_column($metadata, 'componentname'))]; $urlbase = new \moodle_url('/course/mod.php', ['id' => $this->get_course()->id]); $referenceurl = new \moodle_url($urlbase, ['add' => $modname]); if ($referenceurl->out(false) != $itemmetadata->link) { return true; } } return false; } /** * Allows course format to execute code on moodle_page::set_course() * * This function is executed before the output starts. * * If everything is configured correctly, user is redirected from the * default course view page to the activity view page. * * "Section 1" is the administrative page to manage orphaned activities * * If user is on course view page and there is no module added to the course * and the user has 'moodle/course:manageactivities' capability, redirect to create module * form. * * @param moodle_page $page instance of page calling set_course */ public function page_set_course(moodle_page $page) { global $PAGE; $page->add_body_class('format-'. $this->get_format()); if ($PAGE == $page && $page->has_set_url() && $page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) { $edit = optional_param('edit', -1, PARAM_BOOL); if (($edit == 0 || $edit == 1) && confirm_sesskey()) { // This is a request to turn editing mode on or off, do not redirect here, /course/view.php will do redirection. return; } $cm = $this->get_activity(); $cursection = optional_param('section', null, PARAM_INT); if (!empty($cursection) && has_capability('moodle/course:viewhiddensections', context_course::instance($this->courseid))) { // Display orphaned activities (course view page, section 1). return; } if (!$this->get_activitytype()) { if (has_capability('moodle/course:update', context_course::instance($this->courseid))) { // Teacher is redirected to edit course page. $url = new moodle_url('/course/edit.php', array('id' => $this->courseid)); redirect($url, get_string('erroractivitytype', 'format_singleactivity')); } else { // Student sees an empty course page. return; } } if ($cm === null) { if ($this->can_add_activity()) { // This is a user who has capability to create an activity. if ($this->activity_has_subtypes()) { // Activity has multiple items in the activity chooser, it can not be added automatically. if (optional_param('addactivity', 0, PARAM_INT)) { return; } else { $url = new moodle_url('/course/view.php', array('id' => $this->courseid, 'addactivity' => 1)); redirect($url); } } // Redirect to the add activity form. $url = new moodle_url('/course/mod.php', array('id' => $this->courseid, 'section' => 0, 'sesskey' => sesskey(), 'add' => $this->get_activitytype())); redirect($url); } else { // Student views an empty course page. return; } } else if (!$cm->uservisible || !$cm->url) { // Activity is set but not visible to current user or does not have url. // Display course page (either empty or with availability restriction info). return; } else { // Everything is set up and accessible, redirect to the activity page! redirect($cm->url); } } } /** * Allows course format to execute code on moodle_page::set_cm() * * If we are inside the main module for this course, remove extra node level * from navigation: substitute course node with activity node, move all children * * @param moodle_page $page instance of page calling set_cm */ public function page_set_cm(moodle_page $page) { global $PAGE; parent::page_set_cm($page); if ($PAGE == $page && ($cm = $this->get_activity()) && $cm->uservisible && ($cm->id === $page->cm->id) && ($activitynode = $page->navigation->find($cm->id, navigation_node::TYPE_ACTIVITY)) && ($node = $page->navigation->find($page->course->id, navigation_node::TYPE_COURSE))) { // Substitute course node with activity node, move all children. $node->action = $activitynode->action; $node->type = $activitynode->type; $node->id = $activitynode->id; $node->key = $activitynode->key; $node->isactive = $node->isactive || $activitynode->isactive; $node->icon = null; if ($activitynode->children->count()) { foreach ($activitynode->children as &$child) { $child->remove(); $node->add_node($child); } } else { $node->search_for_active_node(); } $activitynode->remove(); } } /** * Returns true if the course has a front page. * * @return boolean false */ public function has_view_page() { return false; } /** * Return the plugin configs for external functions. * * @return array the list of configuration settings * @since Moodle 3.5 */ public function get_config_for_external() { // Return everything (nothing to hide). return $this->get_format_options(); } } PK$9\1W!format/singleactivity/version.phpnu[. /** * Version details * * @package format_singleactivity * @copyright 2012 Marina Glancy * @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 = 'format_singleactivity'; // Full name of the plugin (used for diagnostics) PK$9\wȒ**%format/singleactivity/settingslib.phpnu[. /** * Settings class for format_singleactivity * * @package format_singleactivity * @copyright 2013 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Admin settings class for the format singleactivity activitytype choice * * @package format_singleactivity * @copyright 2013 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class format_singleactivity_admin_setting_activitytype extends admin_setting_configselect { /** * This function may be used in ancestors for lazy loading of choices * * Override this method if loading of choices is expensive, such * as when it requires multiple db requests. * * @return bool true if loaded, false if error */ public function load_choices() { global $CFG; require_once($CFG->dirroot. '/course/format/singleactivity/lib.php'); if (is_array($this->choices)) { return true; } $this->choices = format_singleactivity::get_supported_activities(); return true; } } PK$9\3֗j2format/singleactivity/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for format_singleactivity. * * @package format_singleactivity * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace format_singleactivity\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for format_singleactivity implementing null_provider. * * @copyright 2018 Carlos Escobedo * @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$9\ 1format/singleactivity/classes/output/renderer.phpnu[. namespace format_singleactivity\output; use core_courseformat\output\section_renderer; /** * Renderer for outputting the singleactivity course format. * * @package format_singleactivity * @copyright 2013 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends section_renderer { /** * Displays the activities list in cases when course view page is not * redirected to the activity page. * * @param stdClass $course record from table course * @param bool $orphaned if false displays the main activity (if present) * if true displays all other activities */ public function display($course, $orphaned) { $format = course_get_format($course); $modinfo = $format->get_modinfo(); $cmlistclass = $format->get_output_classname('content\\section\\cmlist'); $output = ''; if ($orphaned) { if (!empty($modinfo->sections[1])) { $output .= $this->output->heading(get_string('orphaned', 'format_singleactivity'), 3, 'sectionname'); $output .= $this->output->box(get_string('orphanedwarning', 'format_singleactivity')); $section = $modinfo->get_section_info(1); $output .= $this->render(new $cmlistclass($format, $section)); } } else { $section = $modinfo->get_section_info(0); $output .= $this->render(new $cmlistclass($format, $section)); if (empty($modinfo->sections[0]) && course_get_format($course)->activity_has_subtypes()) { // Course format was unable to automatically redirect to add module page. $output .= $this->course_section_add_cm_control($course, 0, 0); } } return $output; } } PK$9\&h! ! :format/singleactivity/tests/format_singleactivity_test.phpnu[. namespace format_singleactivity; /** * Single activity course format related unit tests. * * @package format_singleactivity * @copyright 2023 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \format_singleactivity */ final class format_singleactivity_test extends \advanced_testcase { /** * Test for get_view_url(). * * @covers ::get_view_url */ public function test_get_view_url(): void { global $CFG; $this->resetAfterTest(); // Generate a course with two sections (0 and 1) and two modules. $generator = $this->getDataGenerator(); $course1 = $generator->create_course(['format' => 'singleactivity']); course_create_sections_if_missing($course1, [0, 1]); $data = (object)['id' => $course1->id]; $format = course_get_format($course1); $format->update_course_format_options($data); // In page. $this->assertNotEmpty($format->get_view_url(null)); $this->assertNotEmpty($format->get_view_url(0)); $this->assertNotEmpty($format->get_view_url(1)); // Navigation. $this->assertStringContainsString('course/view.php', $format->get_view_url(0)); $this->assertStringContainsString('course/view.php', $format->get_view_url(1)); $this->assertStringContainsString('course/view.php', $format->get_view_url(0, ['navigation' => 1])); $this->assertStringContainsString('course/view.php', $format->get_view_url(1, ['navigation' => 1])); $this->assertStringContainsString('course/view.php', $format->get_view_url(0, ['sr' => 1])); $this->assertStringContainsString('course/view.php', $format->get_view_url(1, ['sr' => 1])); $this->assertStringContainsString('course/view.php', $format->get_view_url(0, ['sr' => 0])); $this->assertStringContainsString('course/view.php', $format->get_view_url(1, ['sr' => 0])); } /** * Test get_required_jsfiles(). * * @covers ::get_required_jsfiles */ public function test_get_required_jsfiles(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(['format' => 'singleactivity']); $format = course_get_format($course); $this->assertEmpty($format->get_required_jsfiles()); } } PK$9\f[[=format/singleactivity/tests/behat/activity_navigation.featurenu[@format @format_singleactivity Feature: Activity navigation in a single activity course In order to quickly switch to another activity in a single activity course with multiple (hidden) activities As a teacher I need to use the activity navigation controls in activities Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | format | activitytype | | Course 1 | C1 | singleactivity | forum | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | section | | assign | Assignment 1 | Test assignment description | C1 | assign1 | 0 | | lesson | Lesson 1 | Test lesson description | C1 | lesson1 | 0 | | forum | Forum 1 | Test forum description | C1 | forum1 | 0 | Scenario: Step through hidden activities in the course as a teacher. Given I log in as "teacher1" When I am on "Course 1" course homepage # The first activity (Forum 1) won't have the previous activity link. Then "#prev-activity-link" "css_element" should not exist And I should see "Assignment 1 (hidden)" in the "#next-activity-link" "css_element" And I follow "Assignment 1 (hidden)" And I should see "Forum 1" in the "#prev-activity-link" "css_element" And I should see "Lesson 1 (hidden)" in the "#next-activity-link" "css_element" And I follow "Lesson 1 (hidden)" And I should see "Assignment 1 (hidden)" in the "#prev-activity-link" "css_element" And "#next-activity-link" "css_element" should not exist Scenario: Jump to a hidden activity as a teacher Given I log in as "teacher1" When I am on "Course 1" course homepage Then "Jump to..." "field" should exist # The current activity (Forum 1) will not be listed. And the "Jump to..." select box should not contain "Forum 1" # Check drop down menu contents. And the "Jump to..." select box should contain "Assignment 1 (hidden)" And the "Jump to..." select box should contain "Lesson 1 (hidden)" # Jump to a hidden activity somewhere in the middle. When I select "Assignment 1 (hidden)" from the "Jump to..." singleselect Then I should see "Assignment 1" And I should see "Forum 1" in the "#prev-activity-link" "css_element" And I should see "Lesson 1 (hidden)" in the "#next-activity-link" "css_element" # Jump to the first activity. And I select "Forum 1" from the "Jump to..." singleselect And I should see "Assignment 1 (hidden)" in the "#next-activity-link" "css_element" But "#prev-activity-link" "css_element" should not exist # Jump to the last activity. And I select "Lesson 1 (hidden)" from the "Jump to..." singleselect And I should see "Assignment 1 (hidden)" in the "#prev-activity-link" "css_element" But "#next-activity-link" "css_element" should not exist Scenario: The activity navigation controls are not available as a student. Given I log in as "student1" And I am on "Course 1" course homepage # The first activity won't have the previous activity link. Then "#prev-activity-link" "css_element" should not exist And "#next-activity-link" "css_element" should not exist And "Jump to..." "field" should not exist Scenario: The activity navigation asks for login to guest user Given I log in as "guest" When I am on "Course 1" course homepage Then I should see "Guests cannot access this course. Please log in" Scenario: The activity navigation asks for login to not enrolled user Given I log in as "student2" When I am on "Course 1" course homepage Then I should see "You cannot enrol yourself in this course" Scenario: The single activity course format supports multilang course names Given the "multilang" filter is "on" And the "multilang" filter applies to "content and headings" When I am on the "Course 1" "course editing" page logged in as "teacher1" And I expand all fieldsets And I set the field "Course full name" in the "General" "fieldset" to "KursCourse 1" And I click on "Save and display" "button" Then I should see "Course 1" in the ".page-header-headings" "css_element" And I should not see "KursCourse 1" in the ".page-header-headings" "css_element" PK$9\a LL7format/singleactivity/tests/behat/create_course.featurenu[@format @format_singleactivity Feature: Courses can be created in Single Activity mode In order to create a single activity course As a manager I need to create courses and set default values on them Scenario: Create a course as a custom course creator Given the following "users" exist: | username | firstname | lastname | email | | kevin | Kevin | the | kevin@example.com | And the following "roles" exist: | shortname | name | archetype | | creator | Creator | | And the following "system role assigns" exist: | user | role | contextlevel | | kevin | creator | System | And the following "role capability" exists: | role | creator | | moodle/course:create | allow | | moodle/course:update | allow | | moodle/course:manageactivities | allow | | moodle/course:viewparticipants | allow | | moodle/role:assign | allow | When I log in as "kevin" And I am on site homepage And I press "Add a new course" And I set the following fields to these values: | Course full name | My first course | | Course short name | myfirstcourse | | Format | Single activity | And I press "Update format" Then I should see "Quiz" in the "Type of activity" "field" And I should see "Forum" in the "Type of activity" "field" # Check that not all the activity types are in the dropdown. And I should not see "Text and media" in the "Type of activity" "field" And I should not see "Subsection" in the "Type of activity" "field" And I should not see "Question bank" in the "Type of activity" "field" And I set the field "Type of activity" to "Quiz" And I press "Save and display" And I should see "New Quiz" PK$9\ujj<format/singleactivity/tests/behat/edit_format_course.featurenu[@format @format_singleactivity Feature: Edit format course to Single Activity format In order to set the format course to single activity course As a teacher I need to edit the course settings and see the dropdown type activity Scenario: Edit a format course as a teacher Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | summary | format | | Course 1 | C1 |

Course summary

| topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" And I am on "Course 1" course homepage When I navigate to "Settings" in current page administration And I set the following fields to these values: | Course full name | My first course | | Course short name | myfirstcourse | | Format | Single activity | And I press "Update format" Then I should see "Forum" in the "Type of activity" "field" # Check that not all the activity types are in the dropdown. And I should not see "Text and media" in the "Type of activity" "field" And I should not see "Subsection" in the "Type of activity" "field" And I press "Save and display" And I should see "New Forum" PK$9\J~c format/singleactivity/format.phpnu[. /** * format.php - course format featuring single activity included from view.php * * if we are not redirected before this point this means we want to * either manage orphaned activities - i.e. display section 1, * or the activity is not setup, does not have url or is not accessible at the * moment * * @package format_singleactivity * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $courserenderer = $PAGE->get_renderer('format_singleactivity'); echo $courserenderer->display($course, $section != 0); PK$9\xZ  "format/singleactivity/settings.phpnu[. /** * Settings for format_singleactivity * * @package format_singleactivity * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot. '/course/format/singleactivity/settingslib.php'); if ($ADMIN->fulltree) { $settings->add(new format_singleactivity_admin_setting_activitytype('format_singleactivity/activitytype', new lang_string('defactivitytype', 'format_singleactivity'), new lang_string('defactivitytypedesc', 'format_singleactivity'), 'forum', null)); } PK$9\ 5Re )format/classes/external/file_handlers.phpnu[. namespace core_courseformat\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/course/dnduploadlib.php'); use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use dndupload_handler; /** * Class for exporting a course file handlers. * * @package core_courseformat * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.2 */ class file_handlers extends external_api { /** * Webservice parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'courseid' => new external_value(PARAM_INT, 'course id', VALUE_REQUIRED), ] ); } /** * Return the list of available file handlers. * * @param int $courseid the course id * @return array of file hanlders. */ public static function execute(int $courseid): array { global $CFG; require_once($CFG->dirroot . '/course/lib.php'); $params = external_api::validate_parameters(self::execute_parameters(), [ 'courseid' => $courseid, ]); $courseid = $params['courseid']; self::validate_context(\context_course::instance($courseid)); $format = course_get_format($courseid); $course = $format->get_course(); $handler = new dndupload_handler($course, null); $data = $handler->get_js_data(); return $data->filehandlers ?? []; } /** * Webservice returns. * * @return external_multiple_structure */ public static function execute_returns(): external_multiple_structure { return new external_multiple_structure( new external_single_structure([ 'extension' => new external_value(PARAM_TEXT, 'File extension'), 'module' => new external_value(PARAM_TEXT, 'Target module'), 'message' => new external_value(PARAM_TEXT, 'Output message'), ]) ); } } PK$9\kv&format/classes/external/new_module.phpnu[. namespace core_courseformat\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_value; use moodle_exception; use coding_exception; use context_course; use core_courseformat\base as course_format; /** * External service to create a new module instance in the course. * * @package core_courseformat * @copyright 2024 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class new_module extends external_api { /** * Webservice parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'courseid' => new external_value(PARAM_INT, 'course id', VALUE_REQUIRED), 'modname' => new external_value(PARAM_ALPHANUMEXT, 'module name', VALUE_REQUIRED), 'targetsectionid' => new external_value(PARAM_INT, 'target section id', VALUE_REQUIRED, null), 'targetcmid' => new external_value(PARAM_INT, 'Optional target cm id', VALUE_DEFAULT, null), ] ); } /** * This webservice will execute the create_module action from the course editor. * * The action will register in a {@see \core_courseformat\stateupdates} all the affected * sections, cms and course attribute. This object (in JSON) will be sent back to the * frontend editor to refresh the updated state elements. * * By default, {@see \core_courseformat\stateupdates} will register only create, delete and update events * on cms, sections and the general course data. However, if some plugin needs adhoc messages for * its own mutation module, extend this class in format_XXX\course. * * @param int $courseid the course id * @param string $modname the module name * @param int $targetsectionid the target section id * @param int|null $targetcmid optional target cm id * @return string Course state in JSON */ public static function execute( int $courseid, string $modname, int $targetsectionid, ?int $targetcmid = null ): string { global $CFG; require_once($CFG->dirroot . '/course/lib.php'); [ 'courseid' => $courseid, 'modname' => $modname, 'targetsectionid' => $targetsectionid, 'targetcmid' => $targetcmid, ] = self::validate_parameters(self::execute_parameters(), [ 'courseid' => $courseid, 'modname' => $modname, 'targetsectionid' => $targetsectionid, 'targetcmid' => $targetcmid, ]); self::validate_context(context_course::instance($courseid)); // Plugin needs to support quick creation and the course format needs to support components. // Formats using YUI modules should not be able to quick-create because the front end cannot react to the change. if (!plugin_supports('mod', $modname, FEATURE_QUICKCREATE) || !course_get_format($courseid)->supports_components()) { throw new moodle_exception("Module $modname does not support quick creation"); } $courseformat = course_get_format($courseid); // Create a course changes tracker object. $defaultupdatesclass = 'core_courseformat\\stateupdates'; $updatesclass = 'format_' . $courseformat->get_format() . '\\courseformat\\stateupdates'; if (!class_exists($updatesclass)) { $updatesclass = $defaultupdatesclass; } $updates = new $updatesclass($courseformat); if (!is_a($updates, $defaultupdatesclass)) { throw new coding_exception("The \"$updatesclass\" class must extend \"$defaultupdatesclass\""); } // Get the actions class from the course format. $actionsclass = 'format_'. $courseformat->get_format().'\\courseformat\\stateactions'; if (!class_exists($actionsclass)) { $actionsclass = 'core_courseformat\\stateactions'; } /** @var \core_courseformat\stateactions $actions */ $actions = new $actionsclass(); $action = 'new_module'; if (!is_callable([$actions, $action])) { throw new moodle_exception("Invalid course state action $action in ".get_class($actions)); } $course = $courseformat->get_course(); // Execute the action. $actions->$action($updates, $course, $modname, $targetsectionid, $targetcmid); // Dispatch the hook for post course content update. $hook = new \core_courseformat\hook\after_course_content_updated( course: $course ); \core\di::get(\core\hook\manager::class)->dispatch($hook); return json_encode($updates); } /** * Webservice returns. * * @return external_value */ public static function execute_returns(): external_value { return new external_value(PARAM_RAW, 'Encoded course update JSON'); } } PK$9\`d55)format/classes/external/update_course.phpnu[. namespace core_courseformat\external; use core\exception\moodle_exception; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_value; use context_course; use core_courseformat\base as course_format; /** * External secrvie to update the course from the course editor components. * * @package core_course * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.0 */ class update_course extends external_api { /** * Webservice parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'action' => new external_value( PARAM_ALPHANUMEXT, 'action: cm_hide, cm_show, section_hide, section_show, cm_moveleft...', VALUE_REQUIRED ), 'courseid' => new external_value(PARAM_INT, 'course id', VALUE_REQUIRED), 'ids' => new external_multiple_structure( new external_value(PARAM_INT, 'Target id'), 'Affected ids', VALUE_DEFAULT, [] ), 'targetsectionid' => new external_value( PARAM_INT, 'Optional target section id', VALUE_DEFAULT, null ), 'targetcmid' => new external_value( PARAM_INT, 'Optional target cm id', VALUE_DEFAULT, null ), ] ); } /** * This webservice will execute any action from the course editor. The default actions * are located in {@see \core_courseformat\stateactions} but the format plugin can extend that class * in format_XXX\course. * * The specific action methods will register in a {@see \core_courseformat\stateupdates} all the affected * sections, cms and course attribute. This object (in JSON) will be sent back to the * frontend editor to refresh the updated state elements. * * By default, {@see \core_courseformat\stateupdates} will register only create, delete and update events * on cms, sections and the general course data. However, if some plugin needs adhoc messages for * its own mutation module, extend this class in format_XXX\course. * * @param string $action the action name to execute * @param int $courseid the course id * @param int[] $ids the affected ids (section or cm depending on the action) * @param int|null $targetsectionid optional target section id (for move action) * @param int|null $targetcmid optional target cm id (for move action) * @return string Course state in JSON */ public static function execute(string $action, int $courseid, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null): string { global $CFG; require_once($CFG->dirroot . '/course/lib.php'); $params = external_api::validate_parameters(self::execute_parameters(), [ 'action' => $action, 'courseid' => $courseid, 'ids' => $ids, 'targetsectionid' => $targetsectionid, 'targetcmid' => $targetcmid, ]); $action = $params['action']; $courseid = $params['courseid']; $ids = $params['ids']; $targetsectionid = $params['targetsectionid']; $targetcmid = $params['targetcmid']; self::validate_context(context_course::instance($courseid)); $format = course_get_format($courseid); $updates = $format->get_stateupdates_instance(); $actions = $format->get_stateactions_instance(); if (!is_callable([$actions, $action])) { throw new moodle_exception("Invalid course state action $action in ".get_class($actions)); } $course = $format->get_course(); // Execute the action. $actions->$action($updates, $course, $ids, $targetsectionid, $targetcmid); // Dispatch the hook for post course content update. $hook = new \core_courseformat\hook\after_course_content_updated( course: $course, ); \core\di::get(\core\hook\manager::class)->dispatch($hook); return json_encode($updates); } /** * Webservice returns. * * @return external_value */ public static function execute_returns(): external_value { return new external_value(PARAM_RAW, 'Encoded course update JSON'); } } PK$9\ϋ)format/classes/external/create_module.phpnu[. namespace core_courseformat\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_value; use moodle_exception; use coding_exception; use context_course; use core_courseformat\base as course_format; /** * External service to create a new module instance in the course. * * @package core_courseformat * @copyright 2024 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 5.0 * @todo Remove this class in Moodle 6.0 MDL-83851 */ class create_module extends external_api { /** * Webservice parameters. * * @deprecated since Moodle 5.0 * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'courseid' => new external_value(PARAM_INT, 'course id', VALUE_REQUIRED), 'modname' => new external_value(PARAM_ALPHANUMEXT, 'module name', VALUE_REQUIRED), 'targetsectionnum' => new external_value(PARAM_INT, 'target section number', VALUE_REQUIRED, null), 'targetcmid' => new external_value(PARAM_INT, 'Optional target cm id', VALUE_DEFAULT, null), ] ); } /** * This webservice will execute the create_module action from the course editor. * * The action will register in a {@see \core_courseformat\stateupdates} all the affected * sections, cms and course attribute. This object (in JSON) will be sent back to the * frontend editor to refresh the updated state elements. * * By default, {@see \core_courseformat\stateupdates} will register only create, delete and update events * on cms, sections and the general course data. However, if some plugin needs adhoc messages for * its own mutation module, extend this class in format_XXX\course. * * @deprecated since Moodle 5.0 * @param int $courseid the course id * @param string $modname the module name * @param int $targetsectionnum the target section number * @param int|null $targetcmid optional target cm id * @return string Course state in JSON */ public static function execute( int $courseid, string $modname, int $targetsectionnum, ?int $targetcmid = null ): string { global $CFG; require_once($CFG->dirroot . '/course/lib.php'); [ 'courseid' => $courseid, 'modname' => $modname, 'targetsectionnum' => $targetsectionnum, 'targetcmid' => $targetcmid, ] = self::validate_parameters(self::execute_parameters(), [ 'courseid' => $courseid, 'modname' => $modname, 'targetsectionnum' => $targetsectionnum, 'targetcmid' => $targetcmid, ]); self::validate_context(context_course::instance($courseid)); // Plugin needs to support quick creation and the course format needs to support components. // Formats using YUI modules should not be able to quick-create because the front end cannot react to the change. if (!plugin_supports('mod', $modname, FEATURE_QUICKCREATE) || !course_get_format($courseid)->supports_components()) { throw new moodle_exception("Module $modname does not support quick creation"); } $courseformat = course_get_format($courseid); // Create a course changes tracker object. $defaultupdatesclass = 'core_courseformat\\stateupdates'; $updatesclass = 'format_' . $courseformat->get_format() . '\\courseformat\\stateupdates'; if (!class_exists($updatesclass)) { $updatesclass = $defaultupdatesclass; } $updates = new $updatesclass($courseformat); if (!is_a($updates, $defaultupdatesclass)) { throw new coding_exception("The \"$updatesclass\" class must extend \"$defaultupdatesclass\""); } // Get the actions class from the course format. $actionsclass = 'format_'. $courseformat->get_format().'\\courseformat\\stateactions'; if (!class_exists($actionsclass)) { $actionsclass = 'core_courseformat\\stateactions'; } $actions = new $actionsclass(); $action = 'create_module'; if (!is_callable([$actions, $action])) { throw new moodle_exception("Invalid course state action $action in ".get_class($actions)); } $course = $courseformat->get_course(); // Execute the action. $actions->$action($updates, $course, $modname, $targetsectionnum, $targetcmid); // Dispatch the hook for post course content update. $hook = new \core_courseformat\hook\after_course_content_updated( course: $course ); \core\di::get(\core\hook\manager::class)->dispatch($hook); return json_encode($updates); } /** * Webservice returns. * * @deprecated since Moodle 5.0 * @return external_value */ public static function execute_returns(): external_value { return new external_value(PARAM_RAW, 'Encoded course update JSON'); } /** * Mark the function as deprecated. * @return bool */ public static function execute_is_deprecated() { return true; } } PK$9\s%format/classes/external/get_state.phpnu[. namespace core_courseformat\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_value; /** * Class for exporting a course state. * * @package core_course * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.0 */ class get_state extends external_api { /** * Webservice parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'courseid' => new external_value(PARAM_INT, 'course id', VALUE_REQUIRED), ]); } /** * This method will load all course, sections and cm states needed to initialize the frontend * course editor module. The state data of every individual course, section and cm is * build using the specifics "state" output components. * * By default, the states are generated by: * - core_courseformat\output\state\course * - core_courseformat\output\state\section * - core_courseformat\output\state\cm * * As the other main course outputs, format plugins can override those output components * to send more information to the frontend course editor. These extended classes should * be located in format_XXX\output\courseformat\state\course, format_XXX\output\courseformat\state\section * or format_XXX\output\courseformat\state\cm. * * @param int $courseid the course id * @return string Course state in JSON */ public static function execute(int $courseid): string { global $PAGE, $CFG, $USER; require_once($CFG->dirroot.'/course/lib.php'); $params = external_api::validate_parameters(self::execute_parameters(), [ 'courseid' => $courseid, ]); $courseid = $params['courseid']; self::validate_context(\context_course::instance($courseid)); $courseformat = course_get_format($courseid); $modinfo = $courseformat->get_modinfo(); $completioninfo = new \completion_info(get_course($courseid)); $istrackeduser = $completioninfo->is_tracked_user($USER->id); // Get the proper renderer. $renderer = $courseformat->get_renderer($PAGE); $result = (object)[ 'course' => (object)[], 'section' => [], 'cm' => [], ]; // Load the output class names. $courseclass = $courseformat->get_output_classname('state\\course'); $sectionclass = $courseformat->get_output_classname('state\\section'); $cmclass = $courseformat->get_output_classname('state\\cm'); // General state. $coursestate = new $courseclass($courseformat); $result->course = $coursestate->export_for_template($renderer); // Sections and course modules state. $sections = $modinfo->get_section_info_all(); foreach ($sections as $section) { if ($courseformat->is_section_visible($section)) { // Only return this section data if it's visible by current user on the course page. $sectionstate = new $sectionclass($courseformat, $section); $result->section[] = $sectionstate->export_for_template($renderer); } } foreach ($modinfo->cms as $cm) { if ($cm->is_visible_on_course_page() && $cm->is_of_type_that_can_display()) { // Only return this course module data if it's visible by current user on the course page. $section = $sections[$cm->sectionnum]; $cmstate = new $cmclass($courseformat, $section, $cm, istrackeduser: $istrackeduser); $result->cm[] = $cmstate->export_for_template($renderer); } } return json_encode($result); } /** * Webservice returns. * * @return external_value */ public static function execute_returns(): external_value { return new external_value(PARAM_RAW, 'Encoded course state JSON'); } } PK$9\әZ Z #format/classes/privacy/provider.phpnu[. namespace core_courseformat\privacy; use core_privacy\local\metadata\collection; /** * Privacy provider implementation for courseformat core subsystem. * * @package core_courseformat * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements // This system has data. \core_privacy\local\metadata\provider, // This system has some sitewide user preferences to export. \core_privacy\local\request\user_preference_provider { /** The user preference for the navigation drawer. */ public const SECTION_PREFERENCES_PREFIX = 'coursesectionspreferences'; /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->add_user_preference( self::SECTION_PREFERENCES_PREFIX, 'privacy:metadata:preference:' . self::SECTION_PREFERENCES_PREFIX ); return $collection; } /** * Store all user preferences for this system. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { // Get user courses. $courses = enrol_get_all_users_courses($userid); if (empty($courses)) { return; } foreach ($courses as $course) { $preferencename = self::SECTION_PREFERENCES_PREFIX . '_' . $course->id; $preference = get_user_preferences($preferencename, null, $userid); if (isset($preference)) { $preferencestring = get_string('preference:' . self::SECTION_PREFERENCES_PREFIX, 'courseformat', $course->fullname); \core_privacy\local\request\writer::export_user_preference( 'core_courseformat', $preferencename, $preference, $preferencestring ); } } } } PK$9\ou format/classes/formatactions.phpnu[. namespace core_courseformat; use core_courseformat\local\courseactions; use core_courseformat\local\sectionactions; use core_courseformat\local\cmactions; use coding_exception; use stdClass; /** * Class to instantiate course format actions. * * This class is used to access course content actions. * * All course actions are divided into three main clases: * - course: actions related to the course. * - section: actions related to the sections. * - cm: actions related to the course modules. * * Format plugin can provide their own actions classes by extending the actions classes * with the following namespaces: * - course: format_{PLUGINNAME}\courseformat\courseactions * - section: format_{PLUGINNAME}\courseformat\sectionactions * - cm: format_{PLUGINNAME}\courseformat\cmactions * * There a static method to get the general formatactions instance: * - formatactions::instance($courseorid): returns an instance to access all available actions. * * The class also provides some convenience methods to get specific actions level on a specific course: * - formatactions::course($courseorid): returns an instance of the course actions class. * - formatactions::section($courseorid): returns an instance of the section actions class. * - formatactions::cm($courseorid): returns an instance of the cm actions class. * * There are two ways of executing actions. For example, to execute a section action * called "move_after" the options are: * * Option A: ideal for executing only one action. * * formatactions::section($courseid)->move_after($sectioninfo, $aftersectioninfo); * * Option B: when actions in the same course are going to be executed at different levels. * * $actions = formatactions::instance($courseid); * $actions->section->move_after($sectioninfo, $aftersectioninfo); * * @package core_courseformat * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class formatactions { /** * @var courseactions|null courseactions instance. */ public courseactions $course; /** * @var sectionactions sectionactions instance. */ public sectionactions $section; /** * @var cmactions cmactions instance. */ public cmactions $cm; /** * Returns an instance of the actions class for the given course format. * * @param base $format the course format. */ protected function __construct(base $format) { $actionclasses = [ 'course' => courseactions::class, 'section' => sectionactions::class, 'cm' => cmactions::class, ]; foreach ($actionclasses as $action => $classname) { $formatalternative = 'format_' . $format->get_format() . '\\courseformat\\' . $action . 'actions'; if (class_exists($formatalternative)) { if (!is_subclass_of($formatalternative, $classname)) { throw new coding_exception("The \"$formatalternative\" must extend \"$classname\""); } $actionclasses[$action] = $formatalternative; } $this->$action = new $actionclasses[$action]($format->get_course()); } } /** * Returns an instance of the actions class for the given course format. * @param int|stdClass $courseorid course id or record. * @return courseactions */ public static function course($courseorid): courseactions { return self::instance($courseorid)->course; } /** * Returns an instance of the actions class for the given course format. * * @param int|stdClass $courseorid course id or record. * @return sectionactions */ public static function section($courseorid): sectionactions { return self::instance($courseorid)->section; } /** * Returns an instance of the actions class for the given course format. * @param int|stdClass $courseorid course id or record. * @return cmactions */ public static function cm($courseorid): cmactions { return self::instance($courseorid)->cm; } /** * Get a course action loader instance. * @param int|stdClass $courseorid course id or course. * @return self */ public static function instance(int|stdClass $courseorid): self { $coursesectionscache = \cache::make('core', 'courseactionsinstances'); $format = base::instance($courseorid); $courseid = $format->get_courseid(); $cachekey = "{$courseid}_{$format->get_format()}"; $cachedinstance = $coursesectionscache->get($cachekey); if ($cachedinstance) { return $cachedinstance; } $result = new self($format); $coursesectionscache->set($cachekey, $result); return $result; } } PK$9\0M M $format/classes/local/baseactions.phpnu[. namespace core_courseformat\local; use core_courseformat\base as course_format; use section_info; use cm_info; use stdClass; /** * Format base actions. * * This class defined the format actions base class extended by the course, section and cm actions. * * It also provides helpers to get the most recent modinfo and format information. Those * convenience methods are meant to improve the actions readability and prevent excessive * message chains. * * @package core_courseformat * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class baseactions { /** * @var stdClass the course object. */ protected stdClass $course; /** * Constructor. * @param stdClass $course the course object. */ public function __construct(stdClass $course) { $this->course = $course; } /** * Get the course. * @return stdClass the course object. */ protected function get_course(): stdClass { return $this->course; } /** * Get the course format. * @return course_format the course format. */ protected function get_format(): course_format { return course_format::instance($this->course); } /** * Get the section info. * @param int $sectionid the section id. * @param int $strictness Use MUST_EXIST to throw exception if it doesn't * @return section_info|null Information for numbered section or null if not found */ protected function get_section_info($sectionid, int $strictness = IGNORE_MISSING): ?section_info { // Course actions must always get the most recent version of the section info. return get_fast_modinfo($this->course->id)->get_section_info_by_id($sectionid, $strictness); } /** * Get the cm info. * @param int $cmid the cm id. * @return cm_info|null Information for numbered cm or null if not found */ protected function get_cm_info($cmid): ?cm_info { // Course actions must always get the most recent version of the cm info. return get_fast_modinfo($this->course->id)->get_cm($cmid); } } PK$9\VK&format/classes/local/courseactions.phpnu[. namespace core_courseformat\local; /** * Course actions. * * @package core_courseformat * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class courseactions extends baseactions { // All general course actions will go here. } PK$9\}@@.format/classes/local/overview/overviewitem.phpnu[. namespace core_courseformat\local\overview; use core\output\renderable; use core\output\renderer_base; use core\output\local\properties\text_align; /** * Class overviewitem * * @package core_courseformat * @copyright 2025 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class overviewitem { /** * Overview item constructor. * * @param string $name The name of the activity. * @param int|string|bool|null $value The section name. * @param string|renderable|null $content The item content. * @param text_align $textalign The preferred text alignment. * @param int $alertcount an optional numeric indicator for alerts (zero means no alerts). * @param string $alertlabel the meaning to show with the alert count. */ public function __construct( /** @var string the name of the activity */ protected string $name, /** @var string the section name */ protected int|string|bool|null $value, /** @var string the item content */ protected string|renderable|null $content = null, /** @var text_align the preferred text alignment. */ protected text_align $textalign = text_align::START, /** @var int an optional numeric indicator for alerts (zero means no alerts). */ protected int $alertcount = 0, /** @var string the badge label an optional label for the badge. */ protected string $alertlabel = '', ) { } /** * Retrieves the name of the overview item. * * @return string */ public function get_name(): string { return $this->name; } /** * Retrieves the value of the overview item. * * @return int|string|bool|null */ public function get_value(): int|string|bool|null { return $this->value; } /** * Gets the content for this item. * * Items can utilize either a renderable object or a pre-rendered string as their content. * * - For simple items, a plain string is sufficient and can be used in any context. * - For more complex items, a renderable object is preferable. This allows the item * to be rendered differently depending on the context, providing greater flexibility. * * @return string|\core\output\renderable|null */ public function get_content(): string|renderable|null { return $this->content ?? (string) $this->value ?? null; } /** * Gets the rendered content for this item. * * This method is used when the context does not have any specific requirements * and could use the default item content rendering. * * @param \core\output\renderer_base $output * @return string */ public function get_rendered_content(renderer_base $output): string { if ($this->content instanceof renderable) { return $output->render($this->content); } return $this->get_content() ?? ''; } /** * Gets the preferred text alignment of the item. * * @return text_align The text alignment. */ public function get_text_align(): text_align { return $this->textalign; } /** * Gets the alert count for the item. * * Alert count is an optional numeric indicator for alerts used for filtering, * highlighting, or the mobile APP badge display. * * @return int The alert count. */ public function get_alert_count(): int { return $this->alertcount; } /** * Gets the alert label for the item. * * @return string The alert label. */ public function get_alert_label(): string { return $this->alertlabel; } /** * Sets the content for this item. * * Items can utilize either a renderable object or a pre-rendered string as their content. * * @param string|renderable|null $content * @return $this */ public function set_content(string|renderable|null $content): static { $this->content = $content; return $this; } /** * Sets the preferred text alignment of the item. * * @param text_align $textalign * @return $this */ public function set_text_align(text_align $textalign): static { $this->textalign = $textalign; return $this; } /** * Sets the value of the overview item. * * @param int|string|bool|null $value * @return $this */ public function set_value(int|string|bool|null $value): static { $this->value = $value; return $this; } /** * Sets the name of the overview item. * * @param string $name * @return $this */ public function set_name(string $name): static { $this->name = $name; return $this; } /** * Sets the alert count and alert label for the item. * * @param int $alertcount * @param string $alertlabel * @return $this */ public function set_alert(int $alertcount, string $alertlabel): static { $this->alertcount = $alertcount; $this->alertlabel = $alertlabel; return $this; } } PK$9\6h7  1format/classes/local/overview/overviewfactory.phpnu[. namespace core_courseformat\local\overview; use cm_info; use core_courseformat\activityoverviewbase; /** * Plugin overview instance factory. * * @package core_courseformat * @copyright 2025 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class overviewfactory { /** * Creates an instance of the appropriate overview class for the given course module. * * @param cm_info $cm The course module information. * @return activityoverviewbase An instance of the appropriate overview class. */ public static function create(cm_info $cm): activityoverviewbase { $classname = "\\mod_{$cm->modname}\\courseformat\\overview"; if (!class_exists($classname)) { $classname = resourceoverview::class; } $result = \core\di::get_container()->make($classname, ['cm' => $cm]); if (!($result instanceof activityoverviewbase)) { throw new \coding_exception("Class $classname must extend " . activityoverviewbase::class); } return $result; } } PK$9\2format/classes/local/overview/resourceoverview.phpnu[. namespace core_courseformat\local\overview; /** * Class resourceoverview * * @package core_courseformat * @copyright 2025 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class resourceoverview extends \core_courseformat\activityoverviewbase { #[\Override] public function get_extra_overview_items(): array { return [ 'type' => $this->get_extra_type_overview(), ]; } /** * Retrieves an overview item for the extra type of the resource. * * @return overviewitem|null */ private function get_extra_type_overview(): ?overviewitem { // Only resource activities shows the type overview // because they are aggregated in one table. $archetype = plugin_supports( type: 'mod', name: $this->cm->modname, feature: FEATURE_MOD_ARCHETYPE, default: MOD_ARCHETYPE_OTHER ); if ($archetype != MOD_ARCHETYPE_RESOURCE) { return null; } return new overviewitem( name: get_string('resource_type'), value: $this->cm->modfullname, content: $this->cm->modfullname, ); } } PK$9\xo`!`!"format/classes/local/cmactions.phpnu[. namespace core_courseformat\local; use core_courseformat\sectiondelegatemodule; use course_modinfo; /** * Course module course format actions. * * @package core_courseformat * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cmactions extends baseactions { /** * Update a course delegated section linked to the given module. * * @param \stdClass $cm * @param array $sectionfields to change in section database record. * @param bool $rebuildcache If true (default), perform a partial cache purge and rebuild. * @return bool true if any delegated section has been updated, false otherwise. */ protected function update_delegated( \stdClass $cm, array $sectionfields, bool $rebuildcache = true ): bool { if (!sectiondelegatemodule::has_delegate_class('mod_' . $cm->modname)) { return false; } // Propagate the changes to delegated section. $cminfo = \cm_info::create($cm); if (!$delegatedsection = $cminfo->get_delegated_section_info()) { return false; } $sectionactions = new sectionactions($this->course); $sectionactions->update($delegatedsection, $sectionfields); if ($rebuildcache) { course_modinfo::purge_course_section_cache_by_id($cm->course, $delegatedsection->id); rebuild_course_cache($cm->course, false, true); } return true; } /** * Rename a course module. * * @param int $cmid the course module id. * @param string $name the new name. * @return bool true if the course module was renamed, false otherwise. */ public function rename(int $cmid, string $name): bool { global $CFG, $DB; require_once($CFG->libdir . '/gradelib.php'); $paramcleaning = empty($CFG->formatstringstriptags) ? PARAM_CLEANHTML : PARAM_TEXT; $name = clean_param($name, $paramcleaning); if (empty($name)) { return false; } if (\core_text::strlen($name) > 255) { throw new \moodle_exception('maximumchars', 'moodle', '', 255); } // The name is stored in the activity instance record. // However, events, gradebook and calendar API uses a legacy // course module data extraction from the DB instead of a section_info. $cm = get_coursemodule_from_id('', $cmid, 0, false, MUST_EXIST); if ($name === $cm->name) { return false; } $DB->update_record( $cm->modname, (object)[ 'id' => $cm->instance, 'name' => $name, 'timemodified' => time(), ] ); $cm->name = $name; $fields = new \stdClass(); $fields->name = $name; \core\event\course_module_updated::create_from_cm($cm)->trigger(); course_modinfo::purge_course_module_cache($cm->course, $cm->id); rebuild_course_cache($cm->course, false, true); $this->update_delegated($cm, ['name' => $name]); // Modules may add some logic to renaming. $modinfo = get_fast_modinfo($cm->course); \core\di::get(\core\hook\manager::class)->dispatch( new \core_courseformat\hook\after_cm_name_edited($modinfo->get_cm($cm->id), $name), ); // Attempt to update the grade item if relevant. $grademodule = $DB->get_record($cm->modname, ['id' => $cm->instance]); $grademodule->cmidnumber = $cm->idnumber; $grademodule->modname = $cm->modname; grade_update_mod_grades($grademodule); // Update calendar events with the new name. course_module_update_calendar_events($cm->modname, $grademodule, $cm); return true; } /** * Update a course module. * * @param int $cmid the course module id. * @param int $visible state of the module * @param int $visibleoncoursepage state of the module on the course page * @param bool $rebuildcache If true (default), perform a partial cache purge and rebuild. * @return bool whether course module was updated */ public function set_visibility(int $cmid, int $visible, int $visibleoncoursepage = 1, bool $rebuildcache = true): bool { global $DB, $CFG; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); if (!$cm = get_coursemodule_from_id('', $cmid, 0, false, MUST_EXIST)) { return false; } // Create events and propagate visibility to associated grade items if the value has changed. // Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades. if ($cm->visible == $visible && $cm->visibleoncoursepage == $visibleoncoursepage) { return true; } if (!$modulename = $DB->get_field('modules', 'name', ['id' => $cm->module])) { return false; } // Updating visible and visibleold to keep them in sync. Only changing a section visibility will // affect visibleold to allow for an original visibility restore. See set_section_visible(). $cminfo = (object)[ 'id' => $cmid, 'visible' => $visible, 'visibleoncoursepage' => $visibleoncoursepage, 'visibleold' => $visible, ]; $DB->update_record('course_modules', $cminfo); $DB->update_record( $cm->modname, (object)[ 'id' => $cm->instance, 'timemodified' => time(), ] ); $fields = ['visible' => $visible, 'visibleold' => $visible]; $this->update_delegated($cm, $fields, false); if ($rebuildcache) { \course_modinfo::purge_course_module_cache($cm->course, $cm->id); rebuild_course_cache($cm->course, false, true); } if ($cm->visible == $visible) { // There is nothing else to change. return true; } if ($events = $DB->get_records('event', ['instance' => $cm->instance, 'modulename' => $modulename])) { foreach ($events as $event) { if ($visible) { $event = new \calendar_event($event); $event->toggle_visibility(true); } else { $event = new \calendar_event($event); $event->toggle_visibility(false); } } } // Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there. // Note that this must be done after updating the row in course_modules, in case // the modules grade_item_update function needs to access $cm->visible. $supportsgrade = plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) && component_callback_exists('mod_' . $modulename, 'grade_item_update'); if ($supportsgrade) { $instance = $DB->get_record($modulename, ['id' => $cm->instance], '*', MUST_EXIST); component_callback('mod_' . $modulename, 'grade_item_update', [$instance]); } else { $gradeitems = \grade_item::fetch_all([ 'itemtype' => 'mod', 'itemmodule' => $modulename, 'iteminstance' => $cm->instance, 'courseid' => $cm->course, ]); if ($gradeitems) { foreach ($gradeitems as $gradeitem) { $gradeitem->set_hidden(!$visible); } } } return true; } } PK$9\JJ'format/classes/local/sectionactions.phpnu[. namespace core_courseformat\local; use section_info; use stdClass; use core\event\course_module_updated; use core\event\course_section_deleted; /** * Section course format actions. * * @package core_courseformat * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sectionactions extends baseactions { /** * Create a course section using a record object. * * If $fields->section is not set, the section is added to the end of the course. * * @param stdClass $fields the fields to set on the section * @param bool $skipcheck the position check has already been made and we know it can be used * @return stdClass the created section record */ protected function create_from_object(stdClass $fields, bool $skipcheck = false): stdClass { global $DB; [ 'position' => $position, 'lastsection' => $lastsection, ] = $this->calculate_positions($fields, $skipcheck); // First add section to the end. $sectionrecord = (object) [ 'course' => $this->course->id, 'section' => $lastsection + 1, 'summary' => $fields->summary ?? '', 'summaryformat' => $fields->summaryformat ?? FORMAT_HTML, 'sequence' => '', 'name' => $fields->name ?? null, 'visible' => $fields->visible ?? 1, 'availability' => $fields->availability ?? null, 'component' => $fields->component ?? null, 'itemid' => $fields->itemid ?? null, 'timemodified' => time(), ]; $sectionrecord->id = $DB->insert_record("course_sections", $sectionrecord); // Now move it to the specified position. if ($position > 0 && $position <= $lastsection) { move_section_to($this->course, $sectionrecord->section, $position, true); $sectionrecord->section = $position; } \core\event\course_section_created::create_from_section($sectionrecord)->trigger(); rebuild_course_cache($this->course->id, true); return $sectionrecord; } /** * Calculate the position and lastsection values. * * Each section number must be unique inside a course. However, the section creation is not always * explicit about the final position. By default, regular sections are created at the last position. * However, delegated section can alter that order, because all delegated sections should have higher * numbers. Apart, restore operations can also create sections with a forced specific number. * * This method returns what is the best position for a new section data and, also, what is the current * last section number. The last section is needed to decide if the new section must be moved or not after * insertion. * * @param stdClass $fields the fields to set on the section * @param bool $skipcheck the position check has already been made and we know it can be used * @return array with the new section position (position key) and the course last section value (lastsection key) */ private function calculate_positions($fields, $skipcheck): array { if (!isset($fields->section)) { $skipcheck = false; } if ($skipcheck) { return [ 'position' => $fields->section, 'lastsection' => $fields->section - 1, ]; } $lastsection = $this->get_last_section_number(); if (!empty($fields->component)) { return [ 'position' => $fields->section ?? $lastsection + 1, 'lastsection' => $lastsection, ]; } return [ 'position' => $fields->section ?? $this->get_last_section_number(false) + 1, 'lastsection' => $lastsection, ]; } /** * Get the last section number in the course. * @param bool $includedelegated whether to include delegated sections * @return int */ protected function get_last_section_number(bool $includedelegated = true): int { global $DB; $delegtadefilter = $includedelegated ? '' : ' AND component IS NULL'; return (int) $DB->get_field_sql( 'SELECT max(section) from {course_sections} WHERE course = ?' . $delegtadefilter, [$this->course->id] ); } /** * Create a delegated section. * * @param string $component the name of the plugin * @param int|null $itemid the id of the delegated section * @param stdClass|null $fields the fields to set on the section * @return section_info the created section */ public function create_delegated( string $component, ?int $itemid = null, ?stdClass $fields = null ): section_info { $record = ($fields) ? clone $fields : new stdClass(); $record->component = $component; $record->itemid = $itemid; $record = $this->create_from_object($record); return $this->get_section_info($record->id); } /** * Creates a course section and adds it to the specified position * * This method returns a section record, not a section_info object. This prevents the regeneration * of the modinfo object each time we create a section. * * If position is greater than number of existing sections, the section is added to the end. * This will become sectionnum of the new section. All existing sections at this or bigger * position will be shifted down. * * @param int $position The position to add to, 0 means to the end. * @param bool $skipcheck the check has already been made and we know that the section with this position does not exist * @return stdClass created section object */ public function create(int $position = 0, bool $skipcheck = false): stdClass { $record = (object) [ 'section' => ($position == 0 && !$skipcheck) ? null : $position, ]; return $this->create_from_object($record, $skipcheck); } /** * Create course sections if they are not created yet. * * The calculations will ignore sections delegated to components. * If the section is created, all delegated sections will be pushed down. * * @param int[] $sectionnums the section numbers to create * @return bool whether any section was created */ public function create_if_missing(array $sectionnums): bool { $result = false; $modinfo = get_fast_modinfo($this->course); // Ensure we add the sections in order. sort($sectionnums); // Delegated sections must be displaced when creating a regular section. $skipcheck = !$modinfo->has_delegated_sections(); $sections = $modinfo->get_section_info_all(); foreach ($sectionnums as $sectionnum) { if (isset($sections[$sectionnum]) && empty($sections[$sectionnum]->component)) { continue; } $this->create($sectionnum, $skipcheck); $result = true; } return $result; } /** * Delete a course section. * @param section_info $sectioninfo the section to delete. * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules. * @param bool $async whether or not to try to delete the section using an adhoc task. Async also depends on a plugin hook. * @return bool whether section was deleted */ public function delete(section_info $sectioninfo, bool $forcedeleteifnotempty = true, bool $async = false): bool { // Check the 'course_module_background_deletion_recommended' hook first. // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested. // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it. // It's up to plugins to handle things like whether or not they are enabled. if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) { foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { if ($pluginfunction()) { return $this->delete_async($sectioninfo, $forcedeleteifnotempty); } } } } return $this->delete_format_data( $sectioninfo, $forcedeleteifnotempty, $this->get_delete_event($sectioninfo) ); } /** * Get the event to trigger when deleting a section. * @param section_info $sectioninfo the section to delete. * @return course_section_deleted the event to trigger */ protected function get_delete_event(section_info $sectioninfo): course_section_deleted { global $DB; // Section record is needed for the event snapshot. $sectionrecord = $DB->get_record('course_sections', ['id' => $sectioninfo->id]); $format = course_get_format($this->course); $sectionname = $format->get_section_name($sectioninfo); $context = \context_course::instance($this->course->id); $event = course_section_deleted::create( [ 'objectid' => $sectioninfo->id, 'courseid' => $this->course->id, 'context' => $context, 'other' => [ 'sectionnum' => $sectioninfo->section, 'sectionname' => $sectionname, ], ] ); $event->add_record_snapshot('course_sections', $sectionrecord); return $event; } /** * Delete a course section. * @param section_info $sectioninfo the section to delete. * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules. * @param course_section_deleted $event the event to trigger * @return bool whether section was deleted */ protected function delete_format_data( section_info $sectioninfo, bool $forcedeleteifnotempty, course_section_deleted $event ): bool { $format = course_get_format($this->course); $result = $format->delete_section($sectioninfo, $forcedeleteifnotempty); if ($result) { $event->trigger(); } rebuild_course_cache($this->course->id, true); return $result; } /** * Course section deletion, using an adhoc task for deletion of the modules it contains. * 1. Schedule all modules within the section for adhoc removal. * 2. Move all modules to course section 0. * 3. Delete the resulting empty section. * * @param section_info $sectioninfo the section to schedule for deletion. * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules. * @return bool true if the section was scheduled for deletion, false otherwise. */ protected function delete_async(section_info $sectioninfo, bool $forcedeleteifnotempty = true): bool { global $DB, $USER; if (!$forcedeleteifnotempty && (!empty($sectioninfo->sequence) || !empty($sectioninfo->summary))) { return false; } // Event needs to be created before the section activities are moved to section 0. $event = $this->get_delete_event($sectioninfo); $affectedmods = $DB->get_records_select( 'course_modules', 'course = ? AND section = ? AND deletioninprogress <> ?', [$this->course->id, $sectioninfo->id, 1], '', 'id' ); // Flag those modules having no existing deletion flag. Some modules may have been // scheduled for deletion manually, and we don't want to create additional adhoc deletion // tasks for these. Moving them to section 0 will suffice. $DB->set_field( 'course_modules', 'deletioninprogress', '1', ['course' => $this->course->id, 'section' => $sectioninfo->id] ); // Move all modules to section 0. $modinfo = get_fast_modinfo($this->course->id); foreach ($modinfo->get_cms() as $cm) { if ($cm->sectionnum == $sectioninfo->section) { moveto_module($cm, $modinfo->get_section_info(0)); } } $removaltask = new \core_course\task\course_delete_modules(); $data = [ 'cms' => $affectedmods, 'userid' => $USER->id, 'realuserid' => \core\session\manager::get_realuser()->id, ]; $removaltask->set_custom_data($data); \core\task\manager::queue_adhoc_task($removaltask); // Ensure we have the latest section info. $sectioninfo = $this->get_section_info($sectioninfo->id); return $this->delete_format_data($sectioninfo, $forcedeleteifnotempty, $event); } /** * Update a course section. * * @param section_info $sectioninfo the section info or database record to update. * @param array|stdClass $fields the fields to update. * @return bool whether section was updated */ public function update(section_info $sectioninfo, array|stdClass $fields): bool { global $DB; $courseid = $this->course->id; // Some fields can not be updated using this method. $fields = array_diff_key((array) $fields, array_flip(['id', 'course', 'section', 'sequence'])); if (array_key_exists('name', $fields) && \core_text::strlen($fields['name']) > 255) { throw new \moodle_exception('maximumchars', 'moodle', '', 255); } // If the section is delegated to a component, it may control some section values. $fields = $this->preprocess_delegated_section_fields($sectioninfo, $fields); if (empty($fields)) { return false; } $fields['id'] = $sectioninfo->id; $fields['timemodified'] = time(); $DB->update_record('course_sections', $fields); $sectioninfo->get_component_instance()?->section_updated((object) $fields); // We need to update the section cache before the format options are updated. \course_modinfo::purge_course_section_cache_by_id($courseid, $sectioninfo->id); rebuild_course_cache($courseid, false, true); course_get_format($courseid)->update_section_format_options($fields); $event = \core\event\course_section_updated::create( [ 'objectid' => $sectioninfo->id, 'courseid' => $courseid, 'context' => \context_course::instance($courseid), 'other' => ['sectionnum' => $sectioninfo->section], ] ); $event->trigger(); if (isset($fields['visible'])) { $this->transfer_visibility_to_cms($sectioninfo, (bool) $fields['visible']); } return true; } /** * Transfer the visibility of the section to the course modules. * * @param section_info $sectioninfo the section info or database record to update. * @param bool $visibility the new visibility of the section. */ protected function transfer_visibility_to_cms(section_info $sectioninfo, bool $visibility): void { global $DB; if (empty($sectioninfo->sequence) || $visibility == (bool) $sectioninfo->visible) { return; } $modules = explode(',', $sectioninfo->sequence); $cmids = []; // In case the section is delegated by a module, we change also the visibility for the source module. $delegateinstance = $sectioninfo->get_component_instance(); if ($delegateinstance) { // We only return sections delegated by course modules. Sections delegated to other // types of components must implement their own methods to get the section. if ($delegateinstance && ($delegateinstance instanceof \core_courseformat\sectiondelegatemodule)) { $delegator = $delegateinstance->get_cm(); $modules[] = $delegator->id; } } foreach ($modules as $moduleid) { $cm = get_coursemodule_from_id(null, $moduleid, $this->course->id); if (!$cm) { continue; } $modupdated = false; if ($visibility) { // As we unhide the section, we use the previously saved visibility stored in visibleold. $modupdated = set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage, false); } else { // We hide the section, so we hide the module but we store the original state in visibleold. $modupdated = set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage, false); if ($modupdated) { $DB->set_field('course_modules', 'visibleold', $cm->visible, ['id' => $moduleid]); } } if ($modupdated) { $cmids[] = $cm->id; } } \course_modinfo::purge_course_modules_cache($this->course->id, $cmids); rebuild_course_cache($this->course->id, false, true); foreach ($cmids as $cmid) { $cm = get_coursemodule_from_id(null, $cmid, $this->course->id); course_module_updated::create_from_cm($cm)->trigger(); } } /** * Preprocess the section fields before updating a delegated section. * * @param section_info $sectioninfo the section info or database record to update. * @param array $fields the fields to update. * @return array the updated fields */ protected function preprocess_delegated_section_fields(section_info $sectioninfo, array $fields): array { $delegated = $sectioninfo->get_component_instance(); if (!$delegated) { return $fields; } if (array_key_exists('name', $fields)) { $fields['name'] = $delegated->preprocess_section_name($sectioninfo, $fields['name']); } return $fields; } } PK$9\h22 format/classes/hook_listener.phpnu[. namespace core_courseformat; use core_courseformat\hook\after_course_content_updated; use core_course\hook\before_course_viewed; use core_group\hook\after_group_membership_added; use core_group\hook\after_group_membership_removed; /** * Hook listener for course format * * @package core_courseformat * @copyright 2024 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class hook_listener { /** * Add members to group room when a new member is added to the group. * * @param after_group_membership_added $hook The group membership added hook. */ public static function add_members_to_group( after_group_membership_added $hook, ): void { $group = $hook->groupinstance; $course = get_course($group->courseid); base::invalidate_all_session_caches_for_course($course); } /** * Remove members from the room when a member is removed from group room. * * @param after_group_membership_removed $hook The group membership removed hook. */ public static function remove_members_from_group( after_group_membership_removed $hook, ): void { $group = $hook->groupinstance; $course = get_course($group->courseid); base::invalidate_all_session_caches_for_course($course); } /** * Redirect to external course url using the before_course_viewed hook. * * @param before_course_viewed $hook The hook object containing course data. * @deprecated since 5.0 * @todo MDL-83839 This method will be removed in Moodle 6.0. */ #[\core\attribute\deprecated('core_courseformat\hook\hook_listener::before_course_viewed', since: '5.0', mdl: 'MDL-83764')] public static function before_course_viewed(before_course_viewed $hook): void { global $CFG; if (file_exists($CFG->dirroot . '/course/externservercourse.php')) { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); include($CFG->dirroot . '/course/externservercourse.php'); if (function_exists('extern_server_course')) { if ($externurl = extern_server_course($hook->course)) { redirect($externurl); } } } } /** * Reset cache for the current user in a course when a course content is updated. * * @param after_course_content_updated $hook The course module created hook. */ public static function course_content_updated( after_course_content_updated $hook, ): void { base::session_cache_reset($hook->course); } /** * Reset cache for the current user in a course when a role is switched. * * @param \core\hook\access\after_role_switched $hook The role switched hook. */ public static function after_role_switched( \core\hook\access\after_role_switched $hook, ): void { if ($coursecontext = $hook->context->get_course_context()) { base::session_cache_reset(get_course($coursecontext->instanceid)); } } /** * Reset cache for the current user when the course completion has been updated. * * @param \core_completion\hook\after_cm_completion_updated $hook The role switched hook. */ public static function after_cm_completion_updated( \core_completion\hook\after_cm_completion_updated $hook, ): void { // The activity completion alters the course state cache for this particular user. $course = get_course($hook->cm->course); if ($course) { base::session_cache_reset($course); } } } PK$9\??format/classes/stateactions.phpnu[. namespace core_courseformat; use core\event\course_module_updated; use cm_info; use section_info; use stdClass; use course_modinfo; use moodle_exception; use context_module; use context_course; /** * Contains the core course state actions. * * The methods from this class should be executed via "core_courseformat_edit" web service. * * Each format plugin could extend this class to provide new actions to the editor. * Extended classes should be locate in "format_XXX\course" namespace and * extends core_courseformat\stateactions. * * @package core_courseformat * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class stateactions { /** * Move course modules to another location in the same course. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids the list of affected course module ids * @param int $targetsectionid optional target section id * @param int $targetcmid optional target cm id * @throws moodle_exception */ public function cm_move( stateupdates $updates, stdClass $course, array $ids, ?int $targetsectionid = null, ?int $targetcmid = null ): void { // Validate target elements. if (!$targetsectionid && !$targetcmid) { throw new moodle_exception("Action cm_move requires targetsectionid or targetcmid"); } $this->validate_cms($course, $ids, __FUNCTION__, ['moodle/course:manageactivities']); // The moveto_module function move elements before a specific target. // To keep the order the movements must be done in descending order (last activity first). $ids = $this->sort_cm_ids_by_course_position($course, $ids, true); // Target cm has more priority than target section. if (!empty($targetcmid)) { $this->validate_cms($course, [$targetcmid], __FUNCTION__); $targetcm = get_fast_modinfo($course)->get_cm($targetcmid); $targetsectionid = $targetcm->section; } else { $this->validate_sections($course, [$targetsectionid], __FUNCTION__); } // The origin sections must be updated as well. $originalsections = []; $beforecmdid = $targetcmid; foreach ($ids as $cmid) { // An updated $modinfo is needed on every loop as activities list change. $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($cmid); $currentsectionid = $cm->section; $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST); if ($targetsection->is_delegated() && $cm->get_delegated_section_info()) { throw new moodle_exception('subsectionmoveerror', 'core'); } $beforecm = (!empty($beforecmdid)) ? $modinfo->get_cm($beforecmdid) : null; if ($beforecm === null || $beforecm->id != $cmid) { moveto_module($cm, $targetsection, $beforecm); } $beforecmdid = $cm->id; $updates->add_cm_put($cm->id); if ($currentsectionid != $targetsectionid) { $originalsections[$currentsectionid] = true; } // If some of the original sections are also target sections, we don't need to update them. if (array_key_exists($targetsectionid, $originalsections)) { unset($originalsections[$targetsectionid]); } } // Use section_state to return the full affected section and activities updated state. $this->cm_state($updates, $course, $ids, $targetsectionid, $targetcmid); foreach (array_keys($originalsections) as $sectionid) { $updates->add_section_put($sectionid); } } /** * Sort the cm ids list depending on the course position. * * Some actions like move should be done in an specific order. * * @param stdClass $course the course object * @param int[] $cmids the array of section $ids * @param bool $descending if the sort order must be descending instead of ascending * @return int[] the array of section ids sorted by section number */ protected function sort_cm_ids_by_course_position( stdClass $course, array $cmids, bool $descending = false ): array { $modinfo = get_fast_modinfo($course); $cmlist = array_keys($modinfo->get_cms()); $cmposition = []; foreach ($cmids as $cmid) { $cmposition[$cmid] = array_search($cmid, $cmlist); } $sorting = ($descending) ? -1 : 1; $sortfunction = function ($acmid, $bcmid) use ($sorting, $cmposition) { return ($cmposition[$acmid] <=> $cmposition[$bcmid]) * $sorting; }; usort($cmids, $sortfunction); return $cmids; } /** * @deprecated since Moodle 4.4 MDL-77038. */ #[\core\attribute\deprecated( replacement: 'stateactions::section_move_after', since: '4.4', mdl: 'MDL-77038', final: true, )] public function section_move( stateupdates $updates, stdClass $course, array $ids, ?int $targetsectionid = null, ?int $targetcmid = null ): void { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } /** * Move course sections after to another location in the same course. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids the list of affected course module ids * @param int $targetsectionid optional target section id * @param int $targetcmid optional target cm id */ public function section_move_after( stateupdates $updates, stdClass $course, array $ids, ?int $targetsectionid = null, ?int $targetcmid = null ): void { // Validate target elements. if (!$targetsectionid) { throw new moodle_exception("Action section_move_after requires targetsectionid"); } $this->validate_sections($course, $ids, __FUNCTION__); $coursecontext = context_course::instance($course->id); require_capability('moodle/course:movesections', $coursecontext); // Section will move after the target section. This means it should be processed in // descending order to keep the relative course order. $this->validate_sections($course, [$targetsectionid], __FUNCTION__); $ids = $this->sort_section_ids_by_section_number($course, $ids, true); $format = course_get_format($course->id); $affectedsections = [$targetsectionid => true]; foreach ($ids as $id) { // An update section_info is needed as section numbers can change on every section movement. $modinfo = get_fast_modinfo($course); $section = $modinfo->get_section_info_by_id($id, MUST_EXIST); $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST); $affectedsections[$section->id] = true; $format->move_section_after($section, $targetsection); } // Use section_state to return the section and activities updated state. $this->section_state($updates, $course, $ids, $targetsectionid); // All course sections can be renamed because of the resort. $modinfo = get_fast_modinfo($course); $allsections = $modinfo->get_section_info_all(); foreach ($allsections as $section) { // Ignore the affected sections because they are already in the updates. if (isset($affectedsections[$section->id])) { continue; } $updates->add_section_put($section->id); } // The section order is at a course level. $updates->add_course_put(); } /** * Sort the sections ids depending on the section number. * * Some actions like move should be done in an specific order. * * @param stdClass $course the course object * @param int[] $sectionids the array of section $ids * @param bool $descending if the sort order must be descending instead of ascending * @return int[] the array of section ids sorted by section number */ protected function sort_section_ids_by_section_number( stdClass $course, array $sectionids, bool $descending = false ): array { $sorting = ($descending) ? -1 : 1; $sortfunction = function ($asection, $bsection) use ($sorting) { return ($asection->section <=> $bsection->section) * $sorting; }; $modinfo = get_fast_modinfo($course); $sections = $this->get_section_info($modinfo, $sectionids); uasort($sections, $sortfunction); return array_keys($sections); } /** * Create a course section. * * This method follows the same logic as changenumsections.php. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids not used * @param int $targetsectionid optional target section id (if not passed section will be appended) * @param int $targetcmid not used */ public function section_add( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $coursecontext = context_course::instance($course->id); require_capability('moodle/course:update', $coursecontext); // Get course format settings. $format = course_get_format($course->id); $lastsectionnumber = $format->get_last_section_number(); $maxsections = $format->get_max_sections(); if ($lastsectionnumber >= $maxsections) { throw new moodle_exception('maxsectionslimit', 'moodle', '', $maxsections); } $modinfo = get_fast_modinfo($course); // Get target section. if ($targetsectionid) { $this->validate_sections($course, [$targetsectionid], __FUNCTION__); $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST); // Inserting sections at any position except in the very end requires capability to move sections. require_capability('moodle/course:movesections', $coursecontext); $insertposition = $targetsection->section + 1; } else { // Get last section. $insertposition = 0; } course_create_section($course, $insertposition); // Adding a section affects the full course structure. $this->course_state($updates, $course); } /** * Delete course sections. * * This method follows the same logic as editsection.php. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids section ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function section_delete( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $coursecontext = context_course::instance($course->id); require_capability('moodle/course:update', $coursecontext); require_capability('moodle/course:movesections', $coursecontext); foreach ($ids as $sectionid) { // We need to get the latest modinfo on each iteration because the section numbers change. $modinfo = get_fast_modinfo($course); $section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST); if (!course_can_delete_section($course, $section)) { continue; } // Send all activity deletions. if (!empty($modinfo->sections[$section->section])) { foreach ($modinfo->sections[$section->section] as $modnumber) { $cm = $modinfo->cms[$modnumber]; $updates->add_cm_remove($cm->id); } } course_delete_section($course, $section, true, true); $updates->add_section_remove($sectionid); } // Removing a section affects the full course structure. $this->course_state($updates, $course); } /** * Hide course sections. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids section ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function section_hide( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $this->set_section_visibility($updates, $course, $ids, 0); } /** * Show course sections. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids section ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function section_show( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $this->set_section_visibility($updates, $course, $ids, 1); } /** * Show course sections. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids section ids * @param int $visible the new visible value */ protected function set_section_visibility( stateupdates $updates, stdClass $course, array $ids, int $visible ) { $this->validate_sections($course, $ids, __FUNCTION__); $coursecontext = context_course::instance($course->id); require_all_capabilities(['moodle/course:update', 'moodle/course:sectionvisibility'], $coursecontext); $modinfo = get_fast_modinfo($course); foreach ($ids as $sectionid) { $section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST); course_update_section($course, $section, ['visible' => $visible]); } $this->section_state($updates, $course, $ids); } /** * Show course cms. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids cm ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function cm_show( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $this->set_cm_visibility($updates, $course, $ids, 1, 1); } /** * Hide course cms. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids cm ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function cm_hide( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $this->set_cm_visibility($updates, $course, $ids, 0, 1); } /** * Stealth course cms. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids cm ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function cm_stealth( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $this->set_cm_visibility($updates, $course, $ids, 1, 0); } /** * Internal method to define the cm visibility. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids cm ids * @param int $visible the new visible value * @param int $coursevisible the new course visible value */ protected function set_cm_visibility( stateupdates $updates, stdClass $course, array $ids, int $visible, int $coursevisible ): void { global $CFG; $this->validate_cms( $course, $ids, __FUNCTION__, ['moodle/course:activityvisibility'] ); $format = course_get_format($course->id); $modinfo = get_fast_modinfo($course); $cms = $this->get_cm_info($modinfo, $ids); foreach ($cms as $cm) { // Check stealth availability. if (!$coursevisible) { $section = $cm->get_section_info(); $allowstealth = !empty($CFG->allowstealth) && $format->allow_stealth_module_visibility($cm, $section); $coursevisible = ($allowstealth) ? 0 : 1; } set_coursemodule_visible($cm->id, $visible, $coursevisible, false); } course_modinfo::purge_course_modules_cache($course->id, $ids); rebuild_course_cache($course->id, false, true); $delegatedsections = []; foreach ($cms as $cm) { $modcontext = context_module::instance($cm->id); course_module_updated::create_from_cm($cm, $modcontext)->trigger(); $updates->add_cm_put($cm->id); if (!$delegatedsection = $cm->get_delegated_section_info()) { continue; } if (!in_array($delegatedsection->id, $delegatedsections)) { $delegatedsections[] = $delegatedsection->id; } } foreach ($delegatedsections as $sectionid => $section) { $updates->add_section_put($sectionid); } } /** * Duplicate a course modules instances into the same course. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids course modules ids to duplicate * @param int|null $targetsectionid optional target section id destination * @param int|null $targetcmid optional target before cm id destination */ public function cm_duplicate( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $this->validate_cms( $course, $ids, __FUNCTION__, ['moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport'], false ); $modinfo = get_fast_modinfo($course); $cms = $this->get_cm_info($modinfo, $ids); // Check capabilities on every activity context. foreach ($cms as $cm) { if (!course_allowed_module($course, $cm->modname)) { throw new moodle_exception('No permission to create that activity'); } } $targetsection = null; if (!empty($targetsectionid)) { $this->validate_sections($course, [$targetsectionid], __FUNCTION__); $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST); } $beforecm = null; if (!empty($targetcmid)) { $this->validate_cms($course, [$targetcmid], __FUNCTION__); $beforecm = $modinfo->get_cm($targetcmid); $targetsection = $modinfo->get_section_info_by_id($beforecm->section, MUST_EXIST); } // Duplicate course modules. $affectedcmids = []; foreach ($cms as $cm) { if ($newcm = duplicate_module($course, $cm)) { if ($targetsection) { moveto_module($newcm, $targetsection, $beforecm); } else { $affectedcmids[] = $newcm->id; } } } if ($targetsection) { $this->section_state($updates, $course, [$targetsection->id]); } else { $this->cm_state($updates, $course, $affectedcmids); } } /** * Delete course cms. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids section ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function cm_delete( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $this->validate_cms($course, $ids, __FUNCTION__, ['moodle/course:manageactivities']); $format = course_get_format($course->id); $modinfo = get_fast_modinfo($course); $affectedsections = []; $cms = $this->get_cm_info($modinfo, $ids); foreach ($cms as $cm) { $section = $cm->get_section_info(); $affectedsections[$section->id] = $section; $format->delete_module($cm, true); $updates->add_cm_remove($cm->id); } foreach ($affectedsections as $sectionid => $section) { $updates->add_section_put($sectionid); } } /** * Move course cms to the right. Indent = 1. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids cm ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function cm_moveright( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $this->set_cm_indentation($updates, $course, $ids, 1); } /** * Move course cms to the left. Indent = 0. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids cm ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function cm_moveleft( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $this->set_cm_indentation($updates, $course, $ids, 0); } /** * Internal method to define the cm indentation level. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids cm ids * @param int $indent new value for indentation */ protected function set_cm_indentation( stateupdates $updates, stdClass $course, array $ids, int $indent ): void { global $DB; $this->validate_cms($course, $ids, __FUNCTION__, ['moodle/course:manageactivities']); $modinfo = get_fast_modinfo($course); $cms = $this->get_cm_info($modinfo, $ids); $cms = $this->filter_cms_with_section_delegate($cms); if (empty($cms)) { return; } list($insql, $inparams) = $DB->get_in_or_equal(array_keys($cms), SQL_PARAMS_NAMED); $DB->set_field_select('course_modules', 'indent', $indent, "id $insql", $inparams); rebuild_course_cache($course->id, false, true); foreach ($cms as $cm) { $modcontext = context_module::instance($cm->id); course_module_updated::create_from_cm($cm, $modcontext)->trigger(); $updates->add_cm_put($cm->id); } } /** * Set NOGROUPS const value to cms groupmode. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids cm ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function cm_nogroups( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $this->set_cm_groupmode($updates, $course, $ids, NOGROUPS); } /** * Set VISIBLEGROUPS const value to cms groupmode. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids cm ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function cm_visiblegroups( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $this->set_cm_groupmode($updates, $course, $ids, VISIBLEGROUPS); } /** * Set SEPARATEGROUPS const value to cms groupmode. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids cm ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function cm_separategroups( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $this->set_cm_groupmode($updates, $course, $ids, SEPARATEGROUPS); } /** * Internal method to define the cm groupmode value. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids cm ids * @param int $groupmode new value for groupmode: NOGROUPS, SEPARATEGROUPS, VISIBLEGROUPS */ protected function set_cm_groupmode( stateupdates $updates, stdClass $course, array $ids, int $groupmode ): void { global $DB; $this->validate_cms($course, $ids, __FUNCTION__, ['moodle/course:manageactivities']); $modinfo = get_fast_modinfo($course); $cms = $this->get_cm_info($modinfo, $ids); list($insql, $inparams) = $DB->get_in_or_equal(array_keys($cms), SQL_PARAMS_NAMED); $DB->set_field_select('course_modules', 'groupmode', $groupmode, "id $insql", $inparams); rebuild_course_cache($course->id, false, true); foreach ($cms as $cm) { $modcontext = context_module::instance($cm->id); course_module_updated::create_from_cm($cm, $modcontext)->trigger(); $updates->add_cm_put($cm->id); } } /** * Extract several cm_info from the course_modinfo. * * @param course_modinfo $modinfo the course modinfo. * @param int[] $ids the course modules $ids * @return cm_info[] the extracted cm_info objects */ protected function get_cm_info(course_modinfo $modinfo, array $ids): array { $cms = []; foreach ($ids as $cmid) { $cms[$cmid] = $modinfo->get_cm($cmid); } return $cms; } /** * Extract several section_info from the course_modinfo. * * @param course_modinfo $modinfo the course modinfo. * @param int[] $ids the course modules $ids * @return section_info[] the extracted section_info objects */ protected function get_section_info(course_modinfo $modinfo, array $ids): array { $sections = []; foreach ($ids as $sectionid) { $sections[$sectionid] = $modinfo->get_section_info_by_id($sectionid); } return $sections; } /** * Update the course content section state to collapse. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids the collapsed section ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function section_content_collapsed( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null, ): void { if (!empty($ids)) { $this->validate_sections($course, $ids, __FUNCTION__); } $format = course_get_format($course->id); $format->add_section_preference_ids('contentcollapsed', $ids); } /** * Update the course content section state to expand. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids the collapsed section ids * @param int|null $targetsectionid not used * @param int|null $targetcmid not used */ public function section_content_expanded( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null, ): void { if (!empty($ids)) { $this->validate_sections($course, $ids, __FUNCTION__); } $format = course_get_format($course->id); $format->remove_section_preference_ids('contentcollapsed', $ids); } /** * Update the course index section state to collapse. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids the collapsed section ids * @param int $targetsectionid not used * @param int $targetcmid not used */ public function section_index_collapsed( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null, ): void { if (!empty($ids)) { $this->validate_sections($course, $ids, __FUNCTION__); } $format = course_get_format($course->id); $format->add_section_preference_ids('indexcollapsed', $ids); } /** * Update the course index section state to expand. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids the collapsed section ids * @param int|null $targetsectionid not used * @param int|null $targetcmid not used */ public function section_index_expanded( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null, ): void { if (!empty($ids)) { $this->validate_sections($course, $ids, __FUNCTION__); } $format = course_get_format($course->id); $format->remove_section_preference_ids('indexcollapsed', $ids); } /** * Add the update messages of the updated version of any cm and section related to the cm ids. * * This action is mainly used by legacy actions to partially update the course state when the * result of core_course_edit_module is not enough to generate the correct state data. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids the list of affected course module ids * @param int $targetsectionid optional target section id * @param int $targetcmid optional target cm id */ public function cm_state( stateupdates $updates, stdClass $course, array $ids, ?int $targetsectionid = null, ?int $targetcmid = null ): void { // Collect all section and cm to return. $cmids = []; foreach ($ids as $cmid) { $cmids[$cmid] = true; } if ($targetcmid) { $cmids[$targetcmid] = true; } $sectionids = []; if ($targetsectionid) { $this->validate_sections($course, [$targetsectionid], __FUNCTION__); $sectionids[$targetsectionid] = true; } $this->validate_cms($course, array_keys($cmids), __FUNCTION__); $modinfo = course_modinfo::instance($course); foreach (array_keys($cmids) as $cmid) { // Add this action to updates array. $updates->add_cm_put($cmid); $cm = $modinfo->get_cm($cmid); $sectionids[$cm->section] = true; } foreach (array_keys($sectionids) as $sectionid) { $updates->add_section_put($sectionid); } } /** * Add the update messages of the updated version of any cm and section related to the section ids. * * This action is mainly used by legacy actions to partially update the course state when the * result of core_course_edit_module is not enough to generate the correct state data. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids the list of affected course section ids * @param int $targetsectionid optional target section id * @param int $targetcmid optional target cm id */ public function section_state( stateupdates $updates, stdClass $course, array $ids, ?int $targetsectionid = null, ?int $targetcmid = null ): void { $cmids = []; if ($targetcmid) { $this->validate_cms($course, [$targetcmid], __FUNCTION__); $cmids[$targetcmid] = true; } $sectionids = []; foreach ($ids as $sectionid) { $sectionids[$sectionid] = true; } if ($targetsectionid) { $sectionids[$targetsectionid] = true; } $this->validate_sections($course, array_keys($sectionids), __FUNCTION__); $modinfo = course_modinfo::instance($course); foreach (array_keys($sectionids) as $sectionid) { $sectioninfo = $modinfo->get_section_info_by_id($sectionid); $updates->add_section_put($sectionid); // Add cms. if (empty($modinfo->sections[$sectioninfo->section])) { continue; } foreach ($modinfo->sections[$sectioninfo->section] as $modnumber) { $mod = $modinfo->cms[$modnumber]; if ($mod->is_visible_on_course_page()) { $cmids[$mod->id] = true; } } } foreach (array_keys($cmids) as $cmid) { // Add this action to updates array. $updates->add_cm_put($cmid); } } /** * Add all the update messages from the complete course state. * * This action is mainly used by legacy actions to partially update the course state when the * result of core_course_edit_module is not enough to generate the correct state data. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param int[] $ids the list of affected course module ids (not used) * @param int $targetsectionid optional target section id (not used) * @param int $targetcmid optional target cm id (not used) */ public function course_state( stateupdates $updates, stdClass $course, array $ids = [], ?int $targetsectionid = null, ?int $targetcmid = null ): void { $modinfo = course_modinfo::instance($course); $updates->add_course_put(); // Add sections updates. $sections = $modinfo->get_section_info_all(); $sectionids = []; foreach ($sections as $sectioninfo) { $sectionids[] = $sectioninfo->id; } if (!empty($sectionids)) { $this->section_state($updates, $course, $sectionids); } } /** * Remove course modules with section delegate from a list. * * @param cm_info[] $cms the list of course modules to filter. * @return cm_info[] the filtered list of course modules indexed by id. */ protected function filter_cms_with_section_delegate(array $cms): array { $filtered = []; $modules = []; foreach ($cms as $cm) { if (!isset($modules[$cm->module])) { $modules[$cm->module] = sectiondelegate::has_delegate_class('mod_' . $cm->modname); } if (!$modules[$cm->module]) { $filtered[$cm->id] = $cm; } } return $filtered; } /** * Checks related to sections: course format support them, all given sections exist and topic 0 is not included. * * @param stdClass $course The course where given $sectionids belong. * @param array $sectionids List of sections to validate. * @param string|null $info additional information in case of error (default null). * @throws moodle_exception if any id is not valid */ protected function validate_sections(stdClass $course, array $sectionids, ?string $info = null): void { global $DB; if (empty($sectionids)) { throw new moodle_exception('emptysectionids', 'core', null, $info); } // No section actions are allowed if course format does not support sections. $courseformat = course_get_format($course->id); if (!$courseformat->uses_sections()) { throw new moodle_exception('sectionactionnotsupported', 'core', null, $info); } list($insql, $inparams) = $DB->get_in_or_equal($sectionids, SQL_PARAMS_NAMED); // Check if all the given sections exist. $couintsections = $DB->count_records_select('course_sections', "id $insql", $inparams); if ($couintsections != count($sectionids)) { throw new moodle_exception('unexistingsectionid', 'core', null, $info); } } /** * Checks related to course modules: all given cm exist and the user has the required capabilities. * * @param stdClass $course The course where given $cmids belong. * @param array $cmids List of course module ids to validate. * @param string $info additional information in case of error. * @param array $capabilities optional capabilities checks to require. * @param bool $usemodcontext whether to use each module context, or the course context * @throws moodle_exception if any id is not valid */ protected function validate_cms( stdClass $course, array $cmids, ?string $info = null, array $capabilities = [], bool $usemodcontext = true, ): void { if (empty($cmids)) { throw new moodle_exception('emptycmids', 'core', null, $info); } $moduleinfo = get_fast_modinfo($course->id); $intersect = array_intersect($cmids, array_keys($moduleinfo->get_cms())); if (count($cmids) != count($intersect)) { throw new moodle_exception('unexistingcmid', 'core', null, $info); } if (!empty($capabilities)) { if ($usemodcontext) { foreach ($cmids as $cmid) { $modcontext = context_module::instance($cmid); require_all_capabilities($capabilities, $modcontext); } } else { $coursecontext = context_course::instance($course->id); require_all_capabilities($capabilities, $coursecontext); } } } /** * Create a course module. * * @deprecated since Moodle 5.0, use new_module instead. * @todo MDL-83851: final deprecation of this method in Moodle 6.0. * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param string $modname the module name * @param int $targetsectionnum target section number * @param int|null $targetcmid optional target cm id */ #[\core\attribute\deprecated( replacement: 'new_module', since: '5.0', mdl: 'MDL-83469', )] public function create_module( stateupdates $updates, stdClass $course, string $modname, int $targetsectionnum, ?int $targetcmid = null ): void { global $CFG; require_once($CFG->dirroot . '/course/modlib.php'); \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); $coursecontext = context_course::instance($course->id); require_capability('moodle/course:update', $coursecontext); // Method "can_add_moduleinfo" called in "prepare_new_moduleinfo_data" will handle the capability checks. [, , , , $moduleinfo] = prepare_new_moduleinfo_data($course, $modname, $targetsectionnum); $moduleinfo->beforemod = $targetcmid; create_module((object) $moduleinfo); // Adding module affects section structure, and if the module has a delegated section even the course structure. $this->course_state($updates, $course); } /** * Create a new course module. * * @param stateupdates $updates the affected course elements track * @param stdClass $course the course object * @param string $modname the module name * @param int $targetsectionid target section id * @param int|null $targetcmid optional target cm id */ public function new_module( stateupdates $updates, stdClass $course, string $modname, int $targetsectionid, ?int $targetcmid = null ): void { global $CFG; require_once($CFG->dirroot . '/course/modlib.php'); $coursecontext = context_course::instance($course->id); require_capability('moodle/course:update', $coursecontext); $modinfo = get_fast_modinfo($course); $section = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST); // Method "can_add_moduleinfo" called in "prepare_new_moduleinfo_data" will handle the capability checks. [, , , , $moduleinfo] = prepare_new_moduleinfo_data($course, $modname, $section->sectionnum); $moduleinfo->beforemod = $targetcmid; create_module((object) $moduleinfo); // Adding module affects section structure, and if the module has a delegated section even the course structure. $this->course_state($updates, $course); } } PK$9\g,format/classes/hook/after_cm_name_edited.phpnu[. namespace core_courseformat\hook; use core\hook\described_hook; use cm_info; /** * Hook for course-module name edited. * * @package core_courseformat * @copyright 2024 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class after_cm_name_edited implements described_hook { /** * Constructor. * * @param cm_info $cm the course module * @param string $newname the new name */ public function __construct( /** @var cm_info the course module */ protected cm_info $cm, /** @var string the new name */ protected string $newname, ) { } /** * Describes the hook purpose. * * @return string */ public static function get_hook_description(): string { return 'This hook is triggered when a course module name is edited.'; } /** * List of tags that describe this hook. * * @return string[] */ public static function get_hook_tags(): array { return ['cm_name_edited']; } /** * Get course module instance. * * @return cm_info */ public function get_cm(): cm_info { return $this->cm; } /** * Get new name. * @return string */ public function get_newname(): string { return $this->newname; } } PK$9\H4format/classes/hook/after_course_content_updated.phpnu[. namespace core_courseformat\hook; use stdClass; /** * Hook after course content has been updated. * * This hook will be dispatched after any content (module, section or other) has been updated in a course. * * @package core_courseformat * @copyright 2024 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions after course content has been updated.')] #[\core\attribute\tags('course')] class after_course_content_updated { /** * Constructor for the hook. * * @param stdClass $course The course instance. */ public function __construct( /** @var stdClass The course instance */ public readonly stdClass $course, ) { } } PK$9\v2(format/classes/sectiondelegatemodule.phpnu[. namespace core_courseformat; use action_menu; use cm_info; use core_courseformat\base as course_format; use core_courseformat\formatactions; use core_courseformat\output\local\content\section\controlmenu; use core_courseformat\stateupdates; use renderer_base; use section_info; use stdClass; /** * Class sectiondelegatemodule * * @package core_courseformat * @copyright 2024 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class sectiondelegatemodule extends sectiondelegate { /** @var section_info $sectioninfo The section_info object of the delegated section module */ /** @var cm_info|null $cm The cm_info object of the delegated section module */ private $cm = null; /** @var stdClass|null $course The course object of the delegated section module */ private $course = null; /** * Constructor. * @param section_info $sectioninfo */ public function __construct( protected section_info $sectioninfo ) { parent::__construct($sectioninfo); /** @var \course_modinfo $modinfo */ $modinfo = $sectioninfo->modinfo; try { // Disabled or missing plugins can throw exceptions. $this->cm = $modinfo->get_instance_of( $this->get_module_name(), $this->sectioninfo->itemid, ); $this->course = $modinfo->get_course(); } catch (\Exception $e) { $this->cm = null; $this->course = null; } } /** * Check if the delegated component is enabled. * * @return bool */ public function is_enabled(): bool { return $this->cm !== null; } /** * Get the delegated section id controlled by a specific cm. * * This method is used when reverse search is needed bu we cannot access the database. * This happens mostly on backup and restore. Do NOT use for normal operations. * * @param stdClass|cm_info $cm a course module compatible data structure. * @return int the section id. */ public static function delegated_section_id(stdClass|cm_info $cm): int { global $DB; return $DB->get_field( 'course_sections', 'id', [ 'course' => $cm->course, 'component' => explode('\\', static::class)[0], 'itemid' => $cm->instance, ], MUST_EXIST ); } /** * Get the parent section of the current delegated section. * * @return section_info|null */ public function get_parent_section(): ?section_info { return $this->cm->get_section_info(); } /** * Get the course object. * * @return cm_info */ public function get_cm(): cm_info { return $this->cm; } /** * Get the course object. * * @return stdClass */ public function get_course(): stdClass { return $this->course; } /** * Get the module name from the section component frankenstyle name. * * @return string */ private function get_module_name(): string { return \core_component::normalize_component($this->sectioninfo->component)[1]; } /** * Sync the section renaming with the activity name. * * @param section_info $section * @param string|null $newname * @return string|null */ public function preprocess_section_name(section_info $section, ?string $newname): ?string { $cm = get_coursemodule_from_instance($this->get_module_name(), $section->itemid); if (!$cm) { return $newname; } if (empty($newname) || $newname === $cm->name) { return $cm->name; } formatactions::cm($section->course)->rename($cm->id, $newname); return $newname; } /** * Allow delegate plugin to modify the available section menu. * * @param course_format $format The course format instance. * @param controlmenu $controlmenu The control menu instance. * @param renderer_base $output The renderer instance. * @return action_menu|null The new action menu with the list of edit control items or null if no action menu is available. */ public function get_section_action_menu( course_format $format, controlmenu $controlmenu, renderer_base $output, ): ?action_menu { $controlmenuclass = $format->get_output_classname('content\\cm\\delegatedcontrolmenu'); $controlmenu = new $controlmenuclass( $format, $this->sectioninfo, $this->cm, ); return $controlmenu->get_action_menu($output); } /** * Add extra state updates when put or create a section. * * @param section_info $section the affected section. * @param stateupdates $updates the state updates object to notify the UI. */ public function put_section_state_extra_updates(section_info $section, stateupdates $updates): void { $cm = get_coursemodule_from_instance($this->get_module_name(), $section->itemid); $updates->add_cm_put($cm->id); } public function section_updated(stdClass $sectionrecord): void { global $DB; $cmrecord = []; if (isset($sectionrecord->availability) && $sectionrecord->availability !== $this->cm->availability) { $cmrecord['availability'] = $sectionrecord->availability; } if (isset($sectionrecord->visible) && $sectionrecord->visible !== $this->cm->visible) { $cmrecord['visible'] = $sectionrecord->visible; $cmrecord['visibleold'] = $sectionrecord->visible; } if (empty($cmrecord)) { return; } $cmrecord['id'] = $this->cm->id; $DB->update_record('course_modules', (object)$cmrecord); } } PK$9\ϋ'format/classes/output/site_renderer.phpnu[. /** * Site topics renderer. * * Site course is not a real course format, but it requires a format renderer to use the output * components. * * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output; class site_renderer extends section_renderer { } PK$9\(format/classes/output/local/state/cm.phpnu[. namespace core_courseformat\output\local\state; use core_courseformat\base as course_format; use completion_info; use core_courseformat\sectiondelegate; use renderer_base; use section_info; use cm_info; use renderable; use stdClass; use core_availability\info_module; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/completionlib.php'); /** * Contains the ajax update course module structure. * * @package core_courseformat * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cm implements renderable { /** * Constructor. */ public function __construct( /** @var course_format $format The course format. */ protected course_format $format, /** @var section_info $section The section data. */ protected section_info $section, /** @var cm_info $cm The course module data. */ protected cm_info $cm, /** @var bool $exportcontent False if pre-rendered cmitem HTML content must be exported. */ protected bool $exportcontent = false, /** @var ?bool $istrackeduser If is_tracked_user is pre-computed for this CM's course, it can be provided here. */ protected ?bool $istrackeduser = null, ) { } /** * Export this data so it can be used as state object in the course editor. * * @param renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(renderer_base $output): stdClass { global $CFG, $USER; $format = $this->format; $section = $this->section; $cm = $this->cm; $course = $format->get_course(); $delegatedsectioninfo = $cm->get_delegated_section_info(); $data = (object)[ 'id' => $cm->id, 'anchor' => "module-{$cm->id}", 'name' => \core_external\util::format_string($cm->name, $cm->context, true), 'visible' => !empty($cm->visible), 'stealth' => $cm->is_stealth(), 'sectionid' => $section->id, 'sectionnumber' => $section->section, 'uservisible' => $cm->uservisible, 'hascmrestrictions' => $this->get_has_restrictions(), 'modname' => get_string('pluginname', 'mod_' . $cm->modname), 'indent' => ($format->uses_indentation()) ? $cm->indent : 0, 'groupmode' => $cm->groupmode, 'module' => $cm->modname, 'plugin' => 'mod_' . $cm->modname, // Activities with delegate section has some restriction to prevent structure loops. 'hasdelegatedsection' => !empty($delegatedsectioninfo), ]; if (!empty($delegatedsectioninfo)) { $data->delegatesectionid = $delegatedsectioninfo->id; } // Check the user access type to this cm. $info = new info_module($cm); $data->accessvisible = ($data->visible && $info->is_available_for_all()); // Add url if the activity is compatible. $url = $cm->url; if ($url) { $data->url = $url->out(); } if ($this->exportcontent) { $data->content = $output->course_section_updated_cm_item($format, $section, $cm); } // Completion status. $completioninfo = new completion_info($course); $data->istrackeduser = $this->istrackeduser ?? $completioninfo->is_tracked_user($USER->id); if ($data->istrackeduser && $completioninfo->is_enabled($cm)) { $completiondata = new \core_completion\cm_completion_details($completioninfo, $cm, $USER->id, false); $data->completionstate = $completiondata->get_overall_completion(); $data->isoverallcomplete = $completiondata->is_overall_complete(); } $data->allowstealth = !empty($CFG->allowstealth) && $format->allow_stealth_module_visibility($cm, $section); return $data; } /** * Return if the activity has a restrictions icon displayed or not. * * @return bool if the activity has visible restrictions for the user. */ protected function get_has_restrictions(): bool { global $CFG; $cm = $this->cm; if (empty($cm->visible) || empty($CFG->enableavailability)) { return false; } // Nothing to be displayed to the user. if (!$cm->is_visible_on_course_page()) { return false; } // Not allowed to see the module but might be allowed to see some availability. if (!$cm->uservisible) { return !empty($cm->availableinfo); } // Content editors can see all restrictions if the activity is visible. if (has_capability('moodle/course:viewhiddenactivities', $cm->context)) { $ci = new info_module($cm); return !empty($ci->get_full_information()); } // Regular users can only see restrictions if apply to them. return false; } } PK$9\sC|..-format/classes/output/local/state/section.phpnu[. namespace core_courseformat\output\local\state; use core_availability\info_section; use core_courseformat\base as course_format; use section_info; use renderable; use stdClass; use context_course; /** * Contains the ajax update section structure. * * @package core_course * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class section implements renderable { /** @var course_format the course format class */ protected $format; /** @var section_info the course section class */ protected $section; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info */ public function __construct(course_format $format, section_info $section) { $this->format = $format; $this->section = $section; } /** * Export this data so it can be used as state object in the course editor. * * @param \renderer_base $output typically, the renderer that's calling this function * @return array data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { $format = $this->format; $course = $format->get_course(); $section = $this->section; $modinfo = $format->get_modinfo(); $indexcollapsed = false; $contentcollapsed = false; $preferences = $format->get_sections_preferences(); if (isset($preferences[$section->id])) { $sectionpreferences = $preferences[$section->id]; if (!empty($sectionpreferences->contentcollapsed)) { $contentcollapsed = true; } if (!empty($sectionpreferences->indexcollapsed)) { $indexcollapsed = true; } } $data = (object)[ 'id' => $section->id, 'section' => $section->section, 'number' => $section->section, 'title' => $format->get_section_name($section), 'hassummary' => !empty($section->summary), 'rawtitle' => $section->name, 'cmlist' => [], 'visible' => !empty($section->visible), 'current' => $format->is_section_current($section), 'indexcollapsed' => $indexcollapsed, 'contentcollapsed' => $contentcollapsed, 'hasrestrictions' => $this->get_has_restrictions(), 'bulkeditable' => $this->is_bulk_editable(), 'component' => $section->component, 'itemid' => $section->itemid, 'parentsectionid' => $section->get_component_instance()?->get_parent_section()?->id, ]; if ($section->uservisible) { $data->sectionurl = course_get_url($course, $section->section, ['navigation' => true])?->out(false); } if (empty($modinfo->sections[$section->section])) { return $data; } foreach ($modinfo->sections[$section->section] as $modnumber) { $mod = $modinfo->cms[$modnumber]; if ($section->uservisible && $mod->is_visible_on_course_page() && $mod->is_of_type_that_can_display()) { $data->cmlist[] = $mod->id; } } return $data; } /** * Return if the section can be selected for bulk editing. * @return bool if the section can be edited in bulk */ protected function is_bulk_editable(): bool { $section = $this->section; return ($section->section != 0); } /** * Return if the section has a restrictions icon displayed or not. * * @return bool if the section has visible restrictions for the user. */ protected function get_has_restrictions(): bool { global $CFG; $section = $this->section; $course = $this->format->get_course(); $context = context_course::instance($course->id); // Hidden sections have no restriction indicator displayed. if (empty($section->visible) || empty($CFG->enableavailability)) { return false; } // The activity is not visible to the user but it may have some availability information. if (!$section->uservisible) { return !empty($section->availableinfo); } // Course editors can see all restrictions if the section is visible. if (has_capability('moodle/course:viewhiddensections', $context)) { $ci = new info_section($section); return !empty($ci->get_full_information()); } // Regular users can only see restrictions if apply to them. return false; } } PK$9\rN ,format/classes/output/local/state/course.phpnu[. namespace core_courseformat\output\local\state; use core_courseformat\base as course_format; use course_modinfo; use moodle_url; use renderable; use stdClass; /** * Contains the ajax update course structure. * * @package core_course * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course implements renderable { /** @var course_format the course format class */ protected $format; /** * Constructor. * * @param course_format $format the course format */ public function __construct(course_format $format) { $this->format = $format; } /** * Export this data so it can be used as state object in the course editor. * * @param renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { global $CFG; $format = $this->format; $course = $format->get_course(); $context = $format->get_context(); // State must represent always the most updated version of the course. $modinfo = course_modinfo::instance($course); $url = new moodle_url('/course/view.php', ['id' => $course->id]); $maxbytes = get_user_max_upload_file_size($context, $CFG->maxbytes, $course->maxbytes); $data = (object)[ 'id' => $course->id, 'numsections' => $format->get_last_section_number(), 'sectionlist' => [], 'editmode' => $format->show_editor(), 'highlighted' => $format->get_section_highlighted_name(), 'maxsections' => $format->get_max_sections(), 'baseurl' => $url->out(), 'statekey' => course_format::session_cache($course), 'maxbytes' => $maxbytes, 'maxbytestext' => display_size($maxbytes), ]; $sections = $modinfo->get_section_info_all(); foreach ($sections as $section) { if ($format->is_section_visible($section)) { $data->sectionlist[] = $section->id; } } return $data; } } PK$9\E?,5format/classes/output/local/overview/overviewpage.phpnu[. namespace core_courseformat\output\local\overview; use core\context\course as context_course; use core\output\named_templatable; use core\output\renderable; use core\url; use core_course\output\activity_icon; use core_collator; use stdClass; /** * Class overview page * * @package core_course * @copyright 2025 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class overviewpage implements renderable, named_templatable { /** @var context_course the context. */ protected context_course $context; /** * Constructor. * * @param stdClass $course the course object. * @param string[] $expanded the sections to be expanded on load. */ public function __construct( /** @var stdClass the course object */ protected stdClass $course, /** @var string[] the sections to be expanded on load */ protected array $expanded = [], ) { $this->context = context_course::instance($this->course->id); } /** * Gets the URL to the course overview page for a given course and module name. * * @param int $courseid * @param string $modname * @return url */ public static function get_modname_url(int $courseid, string $modname): url { return new url( url: '/course/overview.php', params: ['id' => $courseid, 'expand[]' => $modname], anchor: "{$modname}_overview_collapsible", ); } #[\Override] public function export_for_template(\renderer_base $output): stdClass { $modfullnames = $this->get_course_activities_overview_list(); $elements = []; foreach ($modfullnames as $modname => $modfullname) { $elements[] = $this->export_activity_overview_section_data($output, $modname, $modfullname); } return (object) [ 'elements' => $elements, 'courseid' => $this->course->id, 'contextid' => $this->context->id, ]; } /** * Retrieves a list of course activities overview. * * @return string[] An associative array module name => module plural name. */ private function get_course_activities_overview_list(): array { $modinfo = get_fast_modinfo($this->course); $modfullnames = []; $archetypes = []; foreach ($modinfo->cms as $cm) { // Exclude activities that are not displayed on the course page, // aren't visible or have no view link (e.g. label). // Account for folder being displayed inline. if ( !\course_modinfo::is_mod_type_visible_on_course($cm->modname) || !$cm->uservisible || (!$cm->has_view() && strcmp($cm->modname, 'folder') !== 0) ) { continue; } if (array_key_exists($cm->modname, $modfullnames)) { continue; } if (!array_key_exists($cm->modname, $archetypes)) { $archetypes[$cm->modname] = plugin_supports( type: 'mod', name: $cm->modname, feature: FEATURE_MOD_ARCHETYPE, default: MOD_ARCHETYPE_OTHER ); } if ($archetypes[$cm->modname] == MOD_ARCHETYPE_RESOURCE) { if (!array_key_exists('resource', $modfullnames)) { $modfullnames['resource'] = get_string('resources'); } } else { $modfullnames[$cm->modname] = $cm->modplural; } } core_collator::asort($modfullnames); return $modfullnames; } /** * Exports the data for the activity overview section. * * This function checks if the activity has an overview integration, * and return the data accordingly. * * @param \renderer_base $output * @param string $modname The name of the module. * @param string $modfullname The full name of the module. * @return stdClass The exported data for the activity overview section. */ private function export_activity_overview_section_data( \renderer_base $output, string $modname, string $modfullname ): stdClass { return (object) [ 'fragment' => $this->export_overview_fragment($modname), 'icon' => $this->get_activity_overview_icon($output, $modname), 'name' => $modfullname, 'shortname' => $modname, 'open' => in_array($modname, $this->expanded), ]; } /** * Generates the activity overview icon for a given module. * * @param \renderer_base $output * @param string $modname The name of the module for which the icon is being generated. * @return string The HTML string for the activity overview icon. */ private function get_activity_overview_icon(\renderer_base $output, string $modname): string { // Resource is a generic term for all modules with MOD_ARCHETYPE_RESOURCE. // We group all of them under the mod_page icon. if ($modname === 'resource') { $modname = 'page'; } return $output->render(activity_icon::from_modname($modname)); } /** * Exports an overview fragment for a given module name. * * This function creates and returns an object containing details * about the course overview fragment for the specified module. * * @param string $modname * @return stdClass The exported overview fragment data. */ private function export_overview_fragment(string $modname): stdClass { // If the element is expanded, we don't need to load the fragment. if (in_array($modname, $this->expanded)) { return (object) [ 'preloadedcontent' => course_output_fragment_course_overview([ 'courseid' => $this->course->id, 'modname' => $modname, ]), ]; } return (object) [ 'component' => 'core_course', 'method' => 'overview_table', 'course' => $this->course, 'modname' => $modname, ]; } /** * Get the name of the template to use for this templatable. * * @param \renderer_base $renderer The renderer requesting the template name * @return string */ public function get_template_name(\renderer_base $renderer): string { return 'core_courseformat/local/overview/overviewpage'; } } PK$9\|G 5format/classes/output/local/overview/activityname.phpnu[. namespace core_courseformat\output\local\overview; use cm_info; use core\output\named_templatable; use core\output\renderable; use core\output\renderer_base; use core_courseformat\base as course_format; use stdClass; /** * Class activityname * * @package core_courseformat * @copyright 2025 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activityname implements renderable, named_templatable { /** * Constructor. * * @param cm_info $cm The course module. */ public function __construct( /** @var cm_info The course module. */ protected cm_info $cm, ) { } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output Renderer base. * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { $cm = $this->cm; $section = $this->cm->get_section_info(); $course = $this->cm->get_course(); $format = course_format::instance($course); $result = (object) [ 'activityname' => \core_external\util::format_string($cm->name, $cm->context, true), 'activityurl' => $cm->url, 'hidden' => empty($cm->visible), 'stealth' => $cm->is_stealth(), ]; if ($format->uses_sections() && $section->uservisible) { $result->sectiontitle = $format->get_section_name($section); } return $result; } /** * Get the template name. * * @param renderer_base $renderer Renderer base. * @return string */ public function get_template_name(renderer_base $renderer): string { return 'core_courseformat/local/overview/activityname'; } } PK$9\M-d>format/classes/output/local/overview/missingoverviewnotice.phpnu[. namespace core_courseformat\output\local\overview; use core\output\action_link; use core\output\named_templatable; use core\output\renderable; use core\output\notification; use core\plugin_manager; use core\url; use stdClass; /** * Class missingoverviewnotice * * @package core_courseformat * @copyright 2025 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class missingoverviewnotice implements renderable, named_templatable { /** * Constructor. * * @param stdClass $course The course object. * @param string $modname The module name. */ public function __construct( /** @var stdClass $course the course object */ private stdClass $course, /** @var string $modname the activity module name */ private string $modname, ) { } #[\Override] public function export_for_template(\renderer_base $output): stdClass { if (!$this->activity_has_overview_integration($this->modname)) { return $this->export_legacy_overview($output); } // The notice is not needed for plugins with overview class. return (object) []; } /** * Checks if a given activity module has an overview integration. * * The method search for an integration class named `\mod_{modname}\course\overview`. * * @param string $modname The name of the activity module. * @return bool True if the activity module has an overview integration, false otherwise. */ private function activity_has_overview_integration(string $modname): bool { $classname = 'mod_' . $modname . '\courseformat\overview'; if ($modname === 'resource') { $classname = 'core_courseformat\local\overview\resourceoverview'; } return class_exists($classname); } /** * Exports the legacy overview for a given module. * * This export only applies to modules that do not have an overview integration. * * @param \renderer_base $output * @return stdClass */ private function export_legacy_overview( \renderer_base $output, ): stdClass { $legacyoverview = '/mod/' . $this->modname . '/index.php'; $name = plugin_manager::instance()->plugin_name($this->modname); $link = new action_link( url: new url($legacyoverview, ['id' => $this->course->id]), text: get_string('overview_modname', 'core_course', $name), ); $notification = new notification( message: get_string('overview_missing_notice', 'core_course', $output->render($link)), messagetype: notification::NOTIFY_INFO, closebutton: false, title: get_string('overview_missing_title', 'core_course', $name), titleicon: 'i/circleinfo', ); return (object) [ 'name' => $name, 'shortname' => $this->modname, 'notification' => $notification->export_for_template($output), 'missingoverview' => true, ]; } #[\Override] public function get_template_name(\renderer_base $renderer): string { return 'core_courseformat/local/overview/missingoverviewnotice'; } } PK$9\m0&&6format/classes/output/local/overview/overviewtable.phpnu[. namespace core_courseformat\output\local\overview; use core\output\named_templatable; use core\output\renderable; use core\output\renderer_base; use core\plugin_manager; use core_courseformat\local\overview\overviewitem; use core_courseformat\local\overview\overviewfactory; use cm_info; use stdClass; /** * Class overviewtable * * @package core_courseformat * @copyright 2025 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class overviewtable implements renderable, named_templatable { /** @var array $header the table headers */ private array $headers = []; /** @var array $columnhascontent indicate which columns has content */ private array $columnhascontent = []; /** * Constructor. * * @param stdClass $course the course object. * @param string $modname the module name (or "resources" for generic resources overview). */ public function __construct( /** @var stdClass the course object */ protected stdClass $course, /** @var string the module name (or "resources" for generic resources overview) */ protected string $modname, ) { } #[\Override] public function export_for_template(renderer_base $output): stdClass { $activities = $this->load_all_overviews_from_each_activity($output); $headers = $this->export_headers(); $result = (object) [ 'caption' => $this->get_table_caption(), 'headers' => $headers, 'activities' => $this->export_filtered_overviews($output, $activities, $headers), ]; return $result; } /** * Export the filtered list of headers. * * This method will remove any header that does not have any content. * * @return array */ private function export_headers(): array { return array_values( array_filter($this->headers, function ($header) { return $this->columnhascontent[$header->key]; }) ); } /** * Exports filtered overviews for the given activities and headers. * * @param renderer_base $output * @param array $activities An array of activities, each containing an 'overviews' array and a 'cmid'. * @param array $headers An array of header objects, each containing a 'key' property. * @return array An array of filtered overviews, each containing 'cmid' and 'overviews' with rendered content. */ private function export_filtered_overviews( renderer_base $output, array $activities, array $headers, ): array { $result = []; foreach ($activities as $activity) { $items = []; foreach ($headers as $header) { if (!isset($activity['overviews'][$header->key])) { $items[] = (object) ['content' => '', 'overview' => $header->key]; continue; } /** @var overviewitem $item */ $item = $activity['overviews'][$header->key]; $itemsdata = (object) [ 'content' => $item->get_rendered_content($output), 'overview' => $header->key, 'value' => $item->get_value(), 'textalign' => $item->get_text_align()->classes(), ]; $alertcount = $item->get_alert_count(); if ($alertcount > 0) { $itemsdata->alertcount = $alertcount; $itemsdata->alertlabel = $item->get_alert_label(); } $items[] = $itemsdata; } $result[] = [ 'cmid' => $activity['cmid'], 'overviews' => $items, ]; } return $result; } /** * Loads all overviews from activities for the given course and module name. * * @param renderer_base $output * @return array An array of overviews. */ private function load_all_overviews_from_each_activity(renderer_base $output): array { $result = []; foreach ($this->get_related_course_modules() as $cm) { if (!$this->is_cm_displayable($cm)) { continue; } $result[] = [ 'cmid' => $cm->id, 'overviews' => $this->load_overview_items_from_activity($output, $cm), ]; } return $result; } /** * Get all course modules related to the modname. * * @return cm_info[] */ private function get_related_course_modules(): array { $modinfo = get_fast_modinfo($this->course->id); if ($this->modname == 'resource') { $result = $this->get_all_resource_intances($modinfo); } else { $result = $modinfo->get_instances_of($this->modname); } $modinfo->sort_cm_array($result); return $result; } /** * Get the table caption. * * @return string */ private function get_table_caption(): string { if ($this->modname === 'resource') { $name = get_string('resource'); } else { $pluginman = plugin_manager::instance(); $name = $pluginman->plugin_name($this->modname); } return get_string('overview_table_caption', 'core_course', $name); } /** * Get all resource instances. * * This is a very specific case where the same overview table shows * several plugins at once. This is not a common use case and only * happens for MOD_ARCHETYPE_RESOURCE plugins. * * @param mixed $modinfo * @return array */ private function get_all_resource_intances($modinfo): array { $resources = []; $archetypes = []; foreach ($modinfo->cms as $cm) { if (!array_key_exists($cm->modname, $archetypes)) { $archetypes[$cm->modname] = plugin_supports( type: 'mod', name: $cm->modname, feature: FEATURE_MOD_ARCHETYPE, default: MOD_ARCHETYPE_OTHER ); } if ($archetypes[$cm->modname] == MOD_ARCHETYPE_RESOURCE) { $resources[] = $cm; } } return $resources; } /** * Check if the course module is displayable in the overview table. * * @param cm_info $cm * @return bool */ private function is_cm_displayable(cm_info $cm): bool { // Folder is an exception because it has settings to be displayed in the course // page without having a view link. return $cm->uservisible && ($cm->has_view() || strcmp($cm->modname, 'folder') === 0) && \course_modinfo::is_mod_type_visible_on_course($cm->modname); } /** * Loads overview items from a given activity. * * @param renderer_base $output * @param cm_info $cm * @return array An associative array containing the overview items for the activity. */ private function load_overview_items_from_activity(renderer_base $output, cm_info $cm): array { global $PAGE; $overview = overviewfactory::create($cm); $row = [ 'name' => $overview->get_name_overview($output), 'duedate' => $overview->get_due_date_overview($output), 'completion' => $overview->get_completion_overview($output), ]; $row = array_merge($row, $overview->get_extra_overview_items($output)); $gradeitems = $overview->get_grades_overviews(); if (!empty($gradeitems)) { foreach ($gradeitems as $gradeitem) { $row[$gradeitem->get_name()] = $gradeitem; } } // Actions are always the last column, if any. $row['actions'] = $overview->get_actions_overview($output); $row = array_filter($row, function ($item) { return $item !== null; }); $this->register_columns($row); $result = []; foreach ($row as $key => $item) { $result[$key] = $item; } return $result; } /** * Register a new row into the table. * * @param overviewitem[] $row * @return void */ private function register_columns(array $row): void { foreach ($row as $key => $item) { if (!isset($this->columnhascontent[$key])) { $this->columnhascontent[$key] = false; $this->headers[] = (object) [ 'name' => $item->get_name(), 'key' => $key, 'textalign' => $item->get_text_align()->classes(), ]; } $this->columnhascontent[$key] = $this->columnhascontent[$key] || $item->get_value() !== null; } } /** * Get the template name. * * @param renderer_base $renderer Renderer base. * @return string */ public function get_template_name(renderer_base $renderer): string { return 'core_courseformat/local/overview/overviewtable'; } } PK$9\ҝ5format/classes/output/local/content/bulkedittools.phpnu[. namespace core_courseformat\output\local\content; use core\moodlenet\utilities; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; use stdClass; /** * Contains the bulk editor tools bar. * * @package core_courseformat * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class bulkedittools implements named_templatable, renderable { use courseformat_named_templatable; /** @var core_courseformat\base the course format class */ protected $format; /** * Constructor. * * @param course_format $format the course format */ public function __construct(course_format $format) { $this->format = $format; } /** * Export this data so it can be used as the context for a mustache template (core/inplace_editable). * * @param renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { $format = $this->format; $course = $format->get_course(); $data = (object)[ 'id' => $course->id, 'actions' => $this->get_toolbar_actions(), ]; $data->hasactions = !empty($data->actions); return $data; } /** * Get the toolbar actions. * @return array the array of buttons */ protected function get_toolbar_actions(): array { return array_merge( array_values($this->section_control_items()), array_values($this->cm_control_items()), ); } /** * Generate the bulk edit control items of a course module. * * Format plugins can override the method to add or remove elements * from the toolbar. * * @return array of edit control items */ protected function cm_control_items(): array { global $CFG, $USER; $format = $this->format; $context = $format->get_context(); $user = $USER; $controls = []; if (has_capability('moodle/course:activityvisibility', $context, $user)) { $controls['availability'] = [ 'icon' => 't/show', 'action' => 'cmAvailability', 'name' => get_string('availability'), 'title' => get_string('cmavailability', 'core_courseformat'), 'bulk' => 'cm', ]; } $duplicatecapabilities = ['moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport']; if (has_all_capabilities($duplicatecapabilities, $context, $user)) { $controls['duplicate'] = [ 'icon' => 't/copy', 'action' => 'cmDuplicate', 'name' => get_string('duplicate'), 'title' => get_string('cmsduplicate', 'core_courseformat'), 'bulk' => 'cm', ]; } $hasmanageactivities = has_capability('moodle/course:manageactivities', $context, $user); if ($hasmanageactivities) { $controls['move'] = [ 'icon' => 'i/dragdrop', 'action' => 'moveCm', 'name' => get_string('move'), 'title' => get_string('cmsmove', 'core_courseformat'), 'bulk' => 'cm', ]; $controls['delete'] = [ 'icon' => 'i/delete', 'action' => 'cmDelete', 'name' => get_string('delete'), 'title' => get_string('cmsdelete', 'core_courseformat'), 'bulk' => 'cm', ]; } $usercanshare = utilities::can_user_share($context, $user->id, 'course'); if ($CFG->enablesharingtomoodlenet && $usercanshare) { $controls['sharetomoodlenet'] = [ 'id' => 'cmShareToMoodleNet', 'icon' => 'i/share', 'action' => 'cmShareToMoodleNet', 'name' => get_string('moodlenet:sharetomoodlenet'), 'title' => get_string('moodlenet:sharetomoodlenet'), 'bulk' => 'cm', ]; } return $controls; } /** * Generate the bulk edit control items of a section. * * Format plugins can override the method to add or remove elements * from the toolbar. * * @return array of edit control items */ protected function section_control_items(): array { global $USER; $format = $this->format; $context = $format->get_context(); $sectionreturn = $format->get_sectionnum(); $user = $USER; $controls = []; if (has_capability('moodle/course:sectionvisibility', $context, $user)) { $controls['availability'] = [ 'icon' => 't/show', 'action' => 'sectionAvailability', 'name' => get_string('availability'), 'title' => $this->format->get_format_string('sectionsavailability'), 'bulk' => 'section', ]; } if (!$sectionreturn && has_capability('moodle/course:movesections', $context, $user)) { $controls['move'] = [ 'icon' => 'i/dragdrop', 'action' => 'moveSection', 'name' => get_string('move', 'moodle'), 'title' => $this->format->get_format_string('sectionsmove'), 'bulk' => 'section', ]; } $deletecapabilities = ['moodle/course:movesections', 'moodle/course:update']; if (!$sectionreturn && has_all_capabilities($deletecapabilities, $context, $user)) { $controls['delete'] = [ 'icon' => 'i/delete', 'action' => 'deleteSection', 'name' => get_string('delete'), 'title' => $this->format->get_format_string('sectionsdelete'), 'bulk' => 'section', ]; } return $controls; } } PK$9\Oj>>*format/classes/output/local/content/cm.phpnu[. /** * Contains the default activity list from a section. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content; use cm_info; use context_course; use core\output\named_templatable; use core_availability\info_module; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; use renderer_base; use section_info; use stdClass; /** * Base class to render a course module inside a course format. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cm implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format */ protected $format; /** @var section_info the section object */ protected $section; /** @var cm_info the course module instance */ protected $mod; /** @var array optional display options */ protected $displayoptions; /** @var string the activity name output class name */ protected $cmnameclass; /** @var string the activity control menu class name */ protected $controlmenuclass; /** @var string the activity availability class name */ protected $availabilityclass; /** @var string the activity completion class name */ protected $completionclass; /** @var string the activity visibility class name */ protected $visibilityclass; /** @var string the activity groupmode badge class name */ protected $groupmodeclass; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info * @param cm_info $mod the course module ionfo * @param array $displayoptions optional extra display options */ public function __construct(course_format $format, section_info $section, cm_info $mod, array $displayoptions = []) { $this->format = $format; $this->section = $section; $this->mod = $mod; // Add extra display options. $this->displayoptions = $displayoptions; $this->load_classes(); // Get the necessary classes. $this->cmnameclass = $format->get_output_classname('content\\cm\\cmname'); $this->controlmenuclass = $format->get_output_classname('content\\cm\\controlmenu'); $this->availabilityclass = $format->get_output_classname('content\\cm\\availability'); $this->completionclass = $format->get_output_classname('content\\cm\\completion'); $this->visibilityclass = $format->get_output_classname('content\\cm\\visibility'); $this->groupmodeclass = $format->get_output_classname('content\\cm\\groupmode'); } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(renderer_base $output): stdClass { global $PAGE; $mod = $this->mod; $displayoptions = $this->displayoptions; $data = (object)[ 'grouping' => $mod->get_grouping_label($displayoptions['textclasses']), 'modname' => get_string('pluginname', 'mod_' . $mod->modname), 'url' => $mod->url, 'activityname' => $mod->get_formatted_name(), 'textclasses' => $displayoptions['textclasses'], 'classlist' => [], 'cmid' => $mod->id, 'editing' => $PAGE->user_is_editing(), 'sectionnum' => $this->section->section, 'cmbulk' => !$mod->get_delegated_section_info(), ]; // Add partial data segments. $haspartials = []; $haspartials['cmname'] = $this->add_cm_name_data($data, $output); $haspartials['availability'] = $this->add_availability_data($data, $output); $haspartials['alternative'] = $this->add_alternative_content_data($data, $output); $haspartials['completion'] = $this->add_completion_data($data, $output); $haspartials['dates'] = $this->add_dates_data($data, $output); $haspartials['editor'] = $this->add_editor_data($data, $output); $haspartials['groupmode'] = $this->add_groupmode_data($data, $output); $haspartials['visibility'] = $this->add_visibility_data($data, $output); $this->add_actvitychooserbutton_data($data, $output); $this->add_format_data($data, $haspartials, $output); // Calculated fields. if (!empty($data->url)) { $data->hasurl = true; } return $data; } /** * Add course module name attributes to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool if the cm has name data */ protected function add_cm_name_data(stdClass &$data, renderer_base $output): bool { // Mod inplace name editable. $cmname = new $this->cmnameclass( $this->format, $this->section, $this->mod, null, $this->displayoptions ); $data->cmname = $cmname->export_for_template($output); $data->hasname = $cmname->has_name(); return $data->hasname; } /** * Add the module availability to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool if the cm has mod availability */ protected function add_availability_data(stdClass &$data, renderer_base $output): bool { if (!$this->mod->visible) { $data->modavailability = null; return false; } // Mod availability output class. $availability = new $this->availabilityclass( $this->format, $this->section, $this->mod, $this->displayoptions ); $modavailability = $availability->export_for_template($output); $data->modavailability = $modavailability ?? false; return $availability->has_availability($output); } /** * Add the alternative content to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool if the cm has alternative content */ protected function add_alternative_content_data(stdClass &$data, renderer_base $output): bool { $altcontent = $this->mod->get_formatted_content( ['overflowdiv' => true, 'noclean' => true] ); $data->altcontent = (empty($altcontent)) ? false : $altcontent; $data->afterlink = $this->mod->afterlink; $activitybadgedata = $this->mod->get_activitybadge($output); if (!empty($activitybadgedata)) { $data->activitybadge = $activitybadgedata; } return !empty($data->altcontent); } /** * Add activity dates information to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool the module has completion information */ protected function add_dates_data(stdClass &$data, renderer_base $output): bool { global $USER; $course = $this->mod->get_course(); if (!$course->showactivitydates) { return false; } $activitydates = \core\activity_dates::get_dates_for_module($this->mod, $USER->id); $templatedata = new \core_course\output\activity_dates($activitydates); $data->dates = $templatedata->export_for_template($output); return $data->dates->hasdates; } /** * Add activity completion information to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool the module has completion information */ protected function add_completion_data(stdClass &$data, renderer_base $output): bool { $completion = new $this->completionclass($this->format, $this->section, $this->mod); $templatedata = $completion->export_for_template($output); if ($templatedata) { $data->completion = $templatedata; return true; } return false; } /** * Add activity information to the data structure. * * @param stdClass $data the current cm data reference * @param bool[] $haspartials the result of loading partial data elements * @param renderer_base $output typically, the renderer that's calling this function * @return bool if the cm has format data */ protected function add_format_data(stdClass &$data, array $haspartials, renderer_base $output): bool { $result = false; // Legacy indentation. if (!empty($this->mod->indent) && $this->format->uses_indentation()) { $data->indent = $this->mod->indent; if ($this->mod->indent > 15) { $data->hugeindent = true; $result = true; } } // Stealth and hidden from student. if (!$this->mod->visible) { // This module is hidden but current user has capability to see it. $data->modhiddenfromstudents = true; $result = true; } else if ($this->mod->is_stealth()) { // This module is available but is normally not displayed on the course page // (this user can see it because they can manage it). $data->modstealth = true; $result = true; } // Special inline activity format. if ( $this->mod->has_custom_cmlist_item() && !$haspartials['availability'] && !$haspartials['completion'] && !$haspartials['dates'] && !$haspartials['groupmode'] && !isset($data->modhiddenfromstudents) && !isset($data->modstealth) && !$this->format->show_editor() ) { $data->modinline = true; $result = true; } return $result; } /** * Add course editor attributes to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool if the cm has editor data */ protected function add_editor_data(stdClass &$data, renderer_base $output): bool { $course = $this->format->get_course(); $coursecontext = context_course::instance($course->id); $editcaps = []; if (has_capability('moodle/course:activityvisibility', $coursecontext)) { $editcaps = ['moodle/course:activityvisibility']; } if (!$this->format->show_editor($editcaps)) { return false; } $returnsection = $this->format->get_sectionnum(); // Edit actions. $controlmenu = new $this->controlmenuclass( $this->format, $this->section, $this->mod, $this->displayoptions ); $data->controlmenu = $controlmenu->export_for_template($output) ?? false; if (!$this->format->supports_components()) { // Add the legacy YUI move link. $data->moveicon = course_get_cm_move($this->mod, $returnsection); } return true; } /** * Add group mode information to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool the module has group mode information */ protected function add_groupmode_data(stdClass &$data, renderer_base $output): bool { $groupmode = new $this->groupmodeclass($this->format, $this->section, $this->mod); $data->groupmodeinfo = $groupmode->export_for_template($output); return !empty($data->groupmodeinfo); } /** * Add visibility information to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool if the cm has visibility data */ protected function add_visibility_data(stdClass &$data, renderer_base $output): bool { $visibility = new $this->visibilityclass($this->format, $this->section, $this->mod); $templatedata = $visibility->export_for_template($output); $data->visibility = $templatedata ?? false; if ($templatedata) { return true; } return false; } /** * Add the activity chooser button data to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function */ protected function add_actvitychooserbutton_data(stdClass &$data, renderer_base $output): void { $activitychooserbutton = new \core_course\output\activitychooserbutton($this->section, $this->mod); $data->activitychooserbutton = $activitychooserbutton->export_for_template($output); } /** * Returns the CSS classes for the activity name/content * */ protected function load_classes() { $mod = $this->mod; $linkclasses = ''; $textclasses = ''; if ($mod->uservisible) { $info = new info_module($mod); $conditionalhidden = !$info->is_available_for_all(); $accessiblebutdim = (!$mod->visible || $conditionalhidden) && has_capability('moodle/course:viewhiddenactivities', $mod->context); if ($accessiblebutdim && $conditionalhidden) { $linkclasses .= ' conditionalhidden'; $textclasses .= ' conditionalhidden'; } } $this->displayoptions['linkclasses'] = $linkclasses; $this->displayoptions['textclasses'] = $textclasses; $this->displayoptions['onclick'] = htmlspecialchars_decode($mod->onclick, ENT_QUOTES);; } /** * Get the activity link classes. * * @return string the activity link classes. */ public function get_link_classes(): string { return $this->displayoptions['linkclasses'] ?? ''; } /** * Get the activity text/description classes. * * @return string the activity text classes. */ public function get_text_classes(): string { return $this->displayoptions['textclasses'] ?? ''; } /** * Get the activity onclick code. * * @return string the activity onclick. */ public function get_onclick_code(): string { return $this->displayoptions['onclick']; } } PK$9\F&2format/classes/output/local/content/addsection.phpnu[. /** * Contains the default section course format output class. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use moodle_url; use renderable; use stdClass; /** * Base class to render a course add section buttons. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class addsection implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format class */ protected $format; /** * Constructor. * * @param course_format $format the course format */ public function __construct(course_format $format) { $this->format = $format; } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { // If no editor must be displayed, just return an empty structure. if (!$this->format->show_editor(['moodle/course:update'])) { return new stdClass(); } $format = $this->format; $course = $format->get_course(); $options = $format->get_format_options(); $lastsection = $format->get_last_section_number(); $maxsections = $format->get_max_sections(); // Component based formats handle add section button in the frontend. $show = ($lastsection < $maxsections) || $format->supports_components(); $supportsnumsections = array_key_exists('numsections', $options); if ($supportsnumsections) { $data = $this->get_num_sections_data($output, $lastsection, $maxsections); } else if (course_get_format($course)->uses_sections() && $show) { $data = $this->get_add_section_data($output, $lastsection, $maxsections); } if (count((array)$data)) { $data->showaddsection = true; } return $data; } /** * Get the legacy num section add/remove section buttons data. * * Current course format has 'numsections' option, which is very confusing and we suggest course format * developers to get rid of it (see MDL-57769 on how to do it). * * @param \renderer_base $output typically, the renderer that's calling this function * @param int $lastsection the last section number * @param int $maxsections the maximum number of sections * @return stdClass data context for a mustache template */ protected function get_num_sections_data(\renderer_base $output, int $lastsection, int $maxsections): stdClass { $format = $this->format; $course = $format->get_course(); $data = new stdClass(); if ($lastsection < $maxsections) { $data->increase = (object) [ 'url' => new moodle_url( '/course/changenumsections.php', ['courseid' => $course->id, 'increase' => true, 'sesskey' => sesskey()] ), ]; } if ($course->numsections > 0) { $data->decrease = (object) [ 'url' => new moodle_url( '/course/changenumsections.php', ['courseid' => $course->id, 'increase' => false, 'sesskey' => sesskey()] ), ]; } return $data; } /** * Get the add section button data. * * Current course format does not have 'numsections' option but it has multiple sections suppport. * Display the "Add section" link that will insert a section in the end. * Note to course format developers: inserting sections in the other positions should check both * capabilities 'moodle/course:update' and 'moodle/course:movesections'. * * @param \renderer_base $output typically, the renderer that's calling this function * @param int $lastsection the last section number * @param int $maxsections the maximum number of sections * @return stdClass data context for a mustache template */ protected function get_add_section_data(\renderer_base $output, int $lastsection, int $maxsections): stdClass { $format = $this->format; $course = $format->get_course(); $data = new stdClass(); $addstring = $format->get_format_string('addsection'); $params = ['courseid' => $course->id, 'insertsection' => 0, 'sesskey' => sesskey()]; $singlesection = $this->format->get_sectionnum(); if ($singlesection) { $params['sectionreturn'] = $singlesection; } $data->addsections = (object) [ 'url' => new moodle_url('/course/changenumsections.php', $params), 'title' => $addstring, 'newsection' => $maxsections - $lastsection, 'canaddsection' => $lastsection < $maxsections, ]; return $data; } } PK$9\!&&<format/classes/output/local/content/section/availability.phpnu[. /** * Contains the default section availability output class. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content\section; use context_course; use core_availability_multiple_messages; use core\output\named_templatable; use core_availability\info; use core_availability\info_section; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; use section_info; use stdClass; /** * Base class to render section availability. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class availability implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format class */ protected $format; /** @var section_info the section object */ protected $section; /** @var string the has availability attribute name */ protected $hasavailabilityname; /** @var stdClass|null the instance export data */ protected $data = null; /** @var int Availability excerpt text max size treshold */ protected const AVAILABILITY_EXCERPT_MAXSIZE = 100; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info */ public function __construct(course_format $format, section_info $section) { $this->format = $format; $this->section = $section; $this->hasavailabilityname = 'hasavailability'; } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass|null data context for a mustache template */ public function export_for_template(\renderer_base $output): ?stdClass { $this->build_export_data($output); return $this->data; } /** * Returns if the output has availability info to display. * * @param \renderer_base $output typically, the renderer that's calling this function * @return bool if the element has availability data to display */ public function has_availability(\renderer_base $output): bool { $this->build_export_data($output); $attributename = $this->hasavailabilityname; return $this->data->$attributename; } /** * Protected method to build the export data. * * @param \renderer_base $output typically, the renderer that's calling this function */ protected function build_export_data(\renderer_base $output) { if (!empty($this->data)) { return; } $data = (object) $this->get_info($output); $attributename = $this->hasavailabilityname; $data->$attributename = !empty($data->info); $this->data = $data; } /** * Export this data so it can be used as the context for a mustache template. * * If section is not visible, display the message about that ('Not available * until...', that sort of thing). Otherwise, returns blank. * * For users with the ability to view hidden sections, it shows the * information even though you can view the section and also may include * slightly fuller information (so that teachers can tell when sections * are going to be unavailable etc). This logic is the same as for * activities. * * @param \renderer_base $output typically, the renderer that's calling this function * @return stdclass data context for a mustache template */ protected function get_info(\renderer_base $output): array { global $CFG, $USER; $section = $this->section; $context = context_course::instance($section->course); $canviewhidden = has_capability('moodle/course:viewhiddensections', $context, $USER); $editurl = new \moodle_url( '/course/editsection.php', ['id' => $this->section->id, 'showonly' => 'availabilityconditions'] ); $info = ['editurl' => $editurl->out(false)]; if ($section->is_orphan()) { $info['editing'] = false; } if (!$section->visible) { return []; } else if (!$section->uservisible) { if ($section->availableinfo) { // Note: We only get to this function if availableinfo is non-empty, // so there is definitely something to print. $info['info'] = $this->get_availability_data($output, $section->availableinfo, 'isrestricted'); } } else if ($canviewhidden && !empty($CFG->enableavailability)) { // Check if there is an availability restriction. $ci = new info_section($section); $fullinfo = $ci->get_full_information(); if ($fullinfo) { $info['info'] = $this->get_availability_data($output, $fullinfo, 'isrestricted isfullinfo'); } } return $info; } /** * Get the basic availability information data. * * @param \renderer_base $output typically, the renderer that's calling this function * @param string|core_availability_multiple_messages $availabilityinfo the avalability info * @param string $additionalclasses additional css classes * @return stdClass the availability information data */ protected function get_availability_data($output, $availabilityinfo, $additionalclasses = ''): stdClass { // At this point, availabilityinfo is either a string or a renderable. We need to handle both cases in a different way. if (is_string($availabilityinfo)) { $data = $this->availability_info_from_string($output, $availabilityinfo); } else { $data = $this->availability_info_from_output($output, $availabilityinfo); } $data->classes = $additionalclasses; $additionalclasses = array_filter(explode(' ', $additionalclasses)); if (in_array('ishidden', $additionalclasses)) { $data->ishidden = 1; } else if (in_array('isstealth', $additionalclasses)) { $data->isstealth = 1; } else if (in_array('isrestricted', $additionalclasses)) { $data->isrestricted = 1; if (in_array('isfullinfo', $additionalclasses)) { $data->isfullinfo = 1; } } return $data; } /** * Generate the basic availability information data from a string. * Just shorten availability text to generate the excerpt text. * * @param \renderer_base $output typically, the renderer that's calling this function * @param string $availabilityinfo the avalability info * @return stdClass the availability information data */ protected function availability_info_from_string(\renderer_base $output, string $availabilityinfo): stdClass { $course = $this->format->get_course(); $text = info::format_info($availabilityinfo, $course); $data = ['text' => $text]; if (strlen(html_to_text($text, 0, false)) > self::AVAILABILITY_EXCERPT_MAXSIZE) { $data['excerpt'] = shorten_text($text, self::AVAILABILITY_EXCERPT_MAXSIZE); } return (object) $data; } /** * Generate the basic availability information data from a renderable. * Use the header and the first item to generate the excerpt text. * * @param \renderer_base $output typically, the renderer that's calling this function * @param core_availability_multiple_messages $availabilityinfo the avalability info * @return stdClass the availability information data */ protected function availability_info_from_output( \renderer_base $output, core_availability_multiple_messages $availabilityinfo ): stdClass { $course = $this->format->get_course(); $renderable = new \core_availability\output\availability_info($availabilityinfo); // We need to export_for_template() instead of directly render, to reuse the info for both 'text' and 'excerpt'. $info = $renderable->export_for_template($output); $text = $output->render_from_template('core_availability/availability_info', $info); $data = ['text' => info::format_info($text, $course)]; if (!empty($info->items)) { $excerpttext = $info->header . ' ' . $info->items[0]->header; $data['excerpt'] = info::format_info($excerpttext, $course); } return (object) $data; } /** * @deprecated since Moodle 4.3 MDL-78204. Please use {@see self::get_availability_data} instead. */ #[\core\attribute\deprecated('get_availability_data()', since: '4.3', mdl: 'MDL-78489', final: true)] protected function availability_info() { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } } PK$9\6format/classes/output/local/content/section/cmitem.phpnu[. /** * Contains the default activity item from a section. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content\section; use cm_info; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; use renderer_base; use section_info; use stdClass; /** * Base class to render a section activity in the activities list. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cmitem implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format class */ protected $format; /** @var section_info the course section class */ protected $section; /** @var cm_info the course module to display */ protected $mod; /** @var array optional display options */ protected $displayoptions; /** @var string the cm output class name */ protected $cmclass; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info * @param cm_info $mod the course module ionfo * @param array $displayoptions optional extra display options */ public function __construct(course_format $format, section_info $section, cm_info $mod, array $displayoptions = []) { $this->format = $format; $this->section = $section; $this->mod = $mod; $this->displayoptions = $displayoptions; // Get the necessary classes. $this->cmclass = $format->get_output_classname('content\\cm'); } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { $format = $this->format; $course = $format->get_course(); $mod = $this->mod; $data = new stdClass(); $data->cms = []; $completionenabled = $course->enablecompletion == COMPLETION_ENABLED; $showactivityconditions = $completionenabled && $course->showcompletionconditions == COMPLETION_SHOW_CONDITIONS; $showactivitydates = !empty($course->showactivitydates); // This will apply styles to the course homepage when the activity information output component is displayed. $hasinfo = $showactivityconditions || $showactivitydates; $item = new $this->cmclass($format, $this->section, $mod, $this->displayoptions); return (object)[ 'id' => $mod->id, 'anchor' => "module-{$mod->id}", 'module' => $mod->modname, 'extraclasses' => $mod->extraclasses, 'cmformat' => $item->export_for_template($output), 'hasinfo' => $hasinfo, 'indent' => ($format->uses_indentation()) ? $mod->indent : 0, 'groupmode' => $mod->groupmode, ]; } } PK$9\fFDC C 7format/classes/output/local/content/section/summary.phpnu[. /** * Contains the default section summary (used for multipage format). * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content\section; use context_course; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; use section_info; use stdClass; /** * Base class to render a course section summary. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class summary implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format class */ protected $format; /** @var section_info the course section class */ protected $section; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info */ public function __construct(course_format $format, section_info $section) { $this->format = $format; $this->section = $section; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output typically, the renderer that's calling this function * @return array data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { $section = $this->section; $data = new stdClass(); if ($section->uservisible || $section->visible) { $data->summarytext = $this->format_summary_text(); } return $data; } /** * Generate html for a section summary text * * @return string HTML to output. */ public function format_summary_text(): string { $section = $this->section; $context = context_course::instance($section->course); $summarytext = file_rewrite_pluginfile_urls($section->summary, 'pluginfile.php', $context->id, 'course', 'section', $section->id); $options = new stdClass(); $options->noclean = true; $options->overflowdiv = true; return format_text($summarytext, $section->summaryformat, $options); } } PK$9\ߺ6format/classes/output/local/content/section/cmlist.phpnu[. /** * Contains the default activity list from a section. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content\section; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use moodle_url; use renderable; use section_info; use stdClass; /** * Base class to render a section activity list. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cmlist implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format class */ protected $format; /** @var section_info the course section class */ protected $section; /** @var array optional display options */ protected $displayoptions; /** @var string the item output class name */ protected $itemclass; // TODO remove movehereclass as part of MDL-83530. /** @var optional move here output class */ protected $movehereclass; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info * @param array $displayoptions optional extra display options */ public function __construct(course_format $format, section_info $section, array $displayoptions = []) { $this->format = $format; $this->section = $section; $this->displayoptions = $displayoptions; // Get the necessary classes. $this->itemclass = $format->get_output_classname('content\\section\\cmitem'); } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output typically, the renderer that's calling this function * @return array data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { global $USER; $format = $this->format; $section = $this->section; $course = $format->get_course(); $modinfo = $format->get_modinfo(); $user = $USER; $data = new stdClass(); $data->cms = []; // TODO remove showmovehere and the if clause as part of MDL-83530. $showmovehere = ismoving($course->id); if ($showmovehere) { // By default, non-ajax controls are disabled but in some places like the frontpage // it is necessary to display them. This is a temporal solution while JS is still // optional for course editing. $data->hascms = true; $data->showmovehere = true; $data->strmovefull = strip_tags(get_string("movefull", "", "'$user->activitycopyname'")); $data->movetosectionurl = new moodle_url('/course/mod.php', ['movetosection' => $section->id, 'sesskey' => sesskey()]); $data->movingstr = strip_tags(get_string('activityclipboard', '', $user->activitycopyname)); $data->cancelcopyurl = new moodle_url('/course/mod.php', ['cancelcopy' => 'true', 'sesskey' => sesskey()]); } if (empty($modinfo->sections[$section->section])) { return $data; } foreach ($modinfo->sections[$section->section] as $modnumber) { $mod = $modinfo->cms[$modnumber]; // TODO remove this if as part of MDL-83530. if ($showmovehere && $USER->activitycopy == $mod->id) { // If the old non-ajax move is necessary, we do not print the selected cm. continue; } if ($mod->is_visible_on_course_page() && $mod->is_of_type_that_can_display()) { $item = new $this->itemclass($format, $section, $mod, $this->displayoptions); $data->cms[] = (object)[ 'cmitem' => $item->export_for_template($output), 'moveurl' => new moodle_url('/course/mod.php', array('moveto' => $modnumber, 'sesskey' => sesskey())), ]; } } if (!empty($data->cms)) { $data->hascms = true; } return $data; } } PK$9\ p"9format/classes/output/local/content/section/cmsummary.phpnu[. /** * Contains the default activities summary (used for singlesection format). * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content\section; use completion_info; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; use section_info; use stdClass; /** * Base class to render a course section summary. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cmsummary implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format class */ protected $format; /** @var section_info the course section class */ protected $section; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info */ public function __construct(course_format $format, section_info $section) { $this->format = $format; $this->section = $section; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output typically, the renderer that's calling this function * @return array data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { list($mods, $complete, $total, $showcompletion) = $this->calculate_section_stats(); $totalactivities = array_reduce($mods, fn($carry, $item) => $carry + ($item["count"] ?? 0), 0); $data = (object)[ 'showcompletion' => $showcompletion, 'total' => $total, 'complete' => $complete, 'mods' => array_values($mods), 'totalactivities' => $totalactivities, ]; $data->modprogress = get_string('progresstotal', 'completion', $data); return $data; } /** * Calculate the activities count of the current section. * * @return array with [[count by activity type], completed activities, total of activitites] */ protected function calculate_section_stats(): array { $format = $this->format; $course = $format->get_course(); $section = $this->section; $modinfo = $format->get_modinfo(); $completioninfo = new completion_info($course); $mods = []; $total = 0; $complete = 0; $cmids = $modinfo->sections[$section->section] ?? []; $cancomplete = isloggedin() && !isguestuser(); $showcompletion = false; foreach ($cmids as $cmid) { $thismod = $modinfo->cms[$cmid]; if ($thismod->uservisible) { if (isset($mods[$thismod->modname])) { $mods[$thismod->modname]['name'] = $thismod->modplural; $mods[$thismod->modname]['count']++; } else { $mods[$thismod->modname]['name'] = $thismod->modfullname; $mods[$thismod->modname]['count'] = 1; } if ($cancomplete && $completioninfo->is_enabled($thismod) != COMPLETION_TRACKING_NONE) { $showcompletion = true; $total++; $completiondata = $completioninfo->get_data($thismod, true); if ($completiondata->completionstate == COMPLETION_COMPLETE || $completiondata->completionstate == COMPLETION_COMPLETE_PASS) { $complete++; } } } } return [$mods, $complete, $total, $showcompletion]; } } PK$9\K~:format/classes/output/local/content/section/visibility.phpnu[. namespace core_courseformat\output\local\content\section; use context_course; use core\output\choicelist; use core\output\local\dropdown\status; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use core_courseformat\sectiondelegatemodule; use pix_icon; use renderable; use section_info; use stdClass; /** * Base class to render a section visibility inside a course format. * * @package core_courseformat * @copyright 2024 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class visibility implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format */ protected $format; /** @var section_info the section object */ protected $section; /** * Constructor. * @param course_format $format the course format * @param section_info $section the section info */ public function __construct(course_format $format, section_info $section) { $this->format = $format; $this->section = $section; } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass|null data context for a mustache template */ public function export_for_template(\renderer_base $output): ?stdClass { global $USER; if ($this->section->visible) { return null; } $context = context_course::instance($this->section->course); $data = new stdClass(); if (!has_capability('moodle/course:sectionvisibility', $context, $USER)) { $data->notavailable = true; return $data; } $data->editing = $this->format->show_editor(); if ($this->section->is_orphan()) { $data->editing = false; } $data->notavailable = false; $data->hiddenfromstudents = true; if ($data->editing && $this->is_section_visibility_editable()) { $data->dropwdown = $this->get_visibility_dropdown($output); } else { // The user is editing but cannot edit the visibility on this specific section, $data->editing = false; } return $data; } /** * Check if the section visibility is editable. * * @return bool */ protected function is_section_visibility_editable(): bool { // Delegated section inside a hidden sections are not editable. $parentsection = $this->section->get_component_instance()?->get_parent_section(); if ($parentsection && !$parentsection->visible) { return false; } return true; } /** * Get the section visibility dropdown. * @param \renderer_base $output typically, the renderer that's calling this function * @return array */ protected function get_visibility_dropdown(\renderer_base $output): array { $badgetext = $output->visually_hidden_text(get_string('availability')); $badgetext .= get_string('hiddenfromstudents'); $icon = $this->get_icon('hide'); $choice = new choicelist(); $choice->add_option( 'show', get_string('availability_show', 'core_courseformat'), $this->get_option_data('show', 'sectionShow', 'section_show') ); $choice->add_option( 'hide', get_string('availability_hide', 'core_courseformat'), $this->get_option_data('hide', 'sectionHide', 'section_hide') ); $choice->set_selected_value('hide'); $dropdown = new status( $output->render($icon) . ' ' . $badgetext, $choice, ['dialogwidth' => status::WIDTH['big']], ); return $dropdown->export_for_template($output); } /** * Get the data for the option. * * @param string $name the name of the option * @param string $mutation the mutation name * @param string $stateaction the state action name * @return array */ private function get_option_data(string $name, string $mutation, string $stateaction): array { $format = $this->format; $nonajaxurl = $format->get_update_url( action: $stateaction, ids: [$this->section->id], returnurl: $format->get_view_url($format->get_sectionnum(), ['navigation' => true]), ); return [ 'description' => get_string("availability_{$name}_help", 'core_courseformat'), 'icon' => $this->get_icon($name), 'url' => $nonajaxurl, 'extras' => [ 'data-id' => $this->section->id, 'data-action' => $mutation, ], ]; } /** * Get the icon for the section visibility. * @param string $selected the visibility selected value * @return pix_icon */ protected function get_icon(string $selected): pix_icon { if ($selected === 'hide') { return new pix_icon('t/show', ''); } else { return new pix_icon('t/hide', ''); } } } PK$9\7؃6format/classes/output/local/content/section/header.phpnu[. /** * Contains the default section header format output class. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content\section; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; use section_info; use stdClass; /** * Base class to render a section header. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class header implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format class */ protected $format; /** @var section_info the course section class */ protected $section; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info */ public function __construct(course_format $format, section_info $section) { $this->format = $format; $this->section = $section; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output typically, the renderer that's calling this function * @return array data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { $format = $this->format; $section = $this->section; $course = $format->get_course(); $data = (object)[ 'num' => $section->section, 'id' => $section->id, ]; $data->editing = $format->show_editor(); if ($course->id == SITEID) { $data->title = $output->section_title_without_link($section, $course); $data->sitehome = true; } else { if (is_null($format->get_sectionid()) || $format->get_sectionid() != $section->id) { // All sections are displayed. if (!$data->editing && $section->uservisible) { $data->title = $output->section_title($section, $course); } else { $data->title = $output->section_title_without_link($section, $course); } } else { // Only one section is displayed. $data->displayonesection = true; $data->title = $output->section_title_without_link($section, $course); } } $coursedisplay = $format->get_course_display(); $data->headerdisplaymultipage = ($coursedisplay == COURSE_DISPLAY_MULTIPAGE); if ($section->section > $format->get_last_section_number()) { // Stealth sections (orphaned) has special title. $data->title = get_string('orphanedactivitiesinsectionno', '', $section->section); } if (!$section->visible) { $data->ishidden = true; } if (!$data->editing && $section->uservisible) { $data->url = course_get_url($course, $section->section, ['navigation' => true]); } $data->name = get_section_name($course, $section); $data->selecttext = $format->get_format_string('selectsection', $data->name); if (!$format->get_sectionnum() && !$section->is_delegated()) { $data->sectionbulk = true; } // Delegated sections in main course page need to have h4 tag, h3 otherwise. $data->headinglevel = ($section->get_component_instance() && is_null($format->get_sectionid())) ? 4 : 3; return $data; } } PK$9\gaa;format/classes/output/local/content/section/controlmenu.phpnu[. namespace core_courseformat\output\local\content\section; use core\context\course as context_course; use core\output\action_menu; use core\output\action_menu\link; use core\output\action_menu\link_secondary; use core\output\pix_icon; use core\output\renderer_base; use core_courseformat\base as course_format; use core_courseformat\output\local\content\basecontrolmenu; use core\url; use section_info; /** * Base class to render section controls. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class controlmenu extends basecontrolmenu { /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info */ public function __construct(course_format $format, section_info $section) { parent::__construct($format, $section, null, $section->id); } /** * Generate the action menu element depending on the section. * * Sections controlled by a plugin will delegate the control menu to the delegated section class. * * @param renderer_base $output typically, the renderer that's calling this function * @return action_menu|null the section action menu or null if no action menu is available */ public function get_action_menu(renderer_base $output): ?action_menu { if (!empty($this->menu)) { return $this->menu; } $sectiondelegate = $this->section->get_component_instance(); if ($sectiondelegate) { return $sectiondelegate->get_section_action_menu($this->format, $this, $output); } return $this->get_default_action_menu($output); } /** * Generate the default section action menu. * * This method is public in case some block needs to modify the menu before output it. * * @param renderer_base $output typically, the renderer that's calling this function * @return action_menu|null the section action menu */ public function get_default_action_menu(renderer_base $output): ?action_menu { $controls = $this->section_control_items(); return $this->format_controls($controls); } /** * Generate the edit control items of a section. * * @return array of edit control items */ public function section_control_items() { // TODO remove this if as part of MDL-83530. if (!$this->format->supports_components()) { return $this->section_control_items_legacy(); } $controls = []; $controls['view'] = $this->get_section_view_item(); if (!$this->section->is_orphan()) { $controls['edit'] = $this->get_section_edit_item(); $controls['duplicate'] = $this->get_section_duplicate_item(); $controls['visibility'] = $this->get_section_visibility_item(); $controls['movesection'] = $this->get_section_movesection_item(); $controls['permalink'] = $this->get_section_permalink_item(); } $controls['delete'] = $this->get_section_delete_item(); return $controls; } /** * Retrieves the view item for the section control menu. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_section_view_item(): ?link { // Only show the view link if we are not already in the section view page. if ($this->format->get_sectionid() == $this->section->id) { return null; } return new link_secondary( url: new url('/course/section.php', ['id' => $this->section->id]), icon: new pix_icon('i/viewsection', ''), text: get_string('view'), attributes: ['class' => 'view'], ); } /** * Retrieves the edit item for the section control menu. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_section_edit_item(): ?link { if (!has_capability('moodle/course:update', $this->coursecontext)) { return null; } $url = new url( '/course/editsection.php', [ 'id' => $this->section->id, 'sr' => $this->section->sectionnum, ] ); return new link_secondary( url: $url, icon: new pix_icon('i/settings', ''), text: get_string('editsection'), attributes: ['class' => 'edit'], ); } /** * Retrieves the duplicate item for the section control menu. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_section_duplicate_item(): ?link { if ( $this->section->sectionnum == 0 || !has_capability('moodle/course:update', $this->coursecontext) ) { return null; } $url = new url( $this->baseurl, [ 'sectionid' => $this->section->id, 'duplicatesection' => 1, 'sesskey' => sesskey(), ] ); return new link_secondary( url: $url, icon: new pix_icon('t/copy', ''), text: get_string('duplicate'), attributes: ['class' => 'duplicate'], ); } /** * Retrieves the get_section_visibility_menu_item item for the section control menu. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_section_visibility_item(): ?link { if ( $this->section->sectionnum == 0 || !has_capability('moodle/course:sectionvisibility', $this->coursecontext) ) { return null; } $sectionreturn = $this->format->get_sectionnum(); $strhide = get_string('hide'); $strshow = get_string('show'); if ($this->section->visible) { $stateaction = 'section_hide'; $icon = 'i/show'; $name = $strhide; $attributes = [ 'class' => 'icon editing_showhide', 'data-sectionreturn' => $sectionreturn, 'data-action' => 'sectionHide', 'data-id' => $this->section->id, 'data-icon' => 'i/show', 'data-swapname' => $strshow, 'data-swapicon' => 'i/hide', ]; } else { $stateaction = 'section_show'; $icon = 'i/hide'; $name = $strshow; $attributes = [ 'class' => 'editing_showhide', 'data-sectionreturn' => $sectionreturn, 'data-action' => 'sectionShow', 'data-id' => $this->section->id, 'data-icon' => 'i/hide', 'data-swapname' => $strhide, 'data-swapicon' => 'i/show', ]; } $url = $this->format->get_update_url( action: $stateaction, ids: [$this->section->id], returnurl: $this->baseurl, ); return new link_secondary( url: $url, icon: new pix_icon($icon, ''), text: $name, attributes: $attributes, ); } /** * Retrieves the move item for the section control menu. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_section_movesection_item(): ?link { if ( $this->section->sectionnum == 0 || $this->format->get_sectionid() || !has_capability('moodle/course:movesections', $this->coursecontext) ) { return null; } $url = new url( $this->baseurl, [ 'movesection' => $this->section->sectionnum, 'section' => $this->section->sectionnum, ] ); return new link_secondary( url: $url, icon: new pix_icon('i/dragdrop', ''), text: get_string('move'), attributes: [ // This tool requires ajax and will appear only when the frontend state is ready. 'class' => 'move waitstate', 'data-action' => 'moveSection', 'data-id' => $this->section->id, ], ); } /** * Retrieves the move up for the section control menu. * * This actions only apply to non-component-based formats * or when javascript is not available. * * Note: this action will be removed, do not depend on it for your * custom formats. For more information, see MDL-83562. Use this method * only if your format is not compatible with the move section modal * and you are still migrating to components. * * @deprecated since Moodle 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return link|null The menu item if applicable, otherwise null. */ #[\core\attribute\deprecated( replacement: 'core_courseformat\output\local\content\section::get_section_movesection_item', since: '5.0', reason: 'Non-ajax section move is deprecated.', mdl: 'MDL-83562', )] protected function get_section_moveup_item(): ?link { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); if ( $this->section->sectionnum <= 1 || $this->format->get_sectionid() || !has_capability('moodle/course:movesections', $this->coursecontext) ) { return null; } $url = new url( $this->baseurl, [ 'section' => $this->section->sectionnum, 'move' => -1, 'sesskey' => sesskey(), ] ); return new link_secondary( url: $url, icon: new pix_icon('i/up', ''), text: get_string('moveup'), attributes: [ // This tool disappears when the state is ready whilenostate. 'class' => 'moveup whilenostate', ], ); } /** * Retrieves the move down for the section control menu. * * This actions only apply to non-component-based formats * or when javascript is not available. * * Note: this action will be removed, do not depend on it for your * custom formats. For more information, see MDL-83562. * * @deprecated since Moodle 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return link|null The menu item if applicable, otherwise null. */ #[\core\attribute\deprecated( replacement: 'core_courseformat\output\local\content\section::get_section_movesection_item', since: '5.0', reason: 'Non-ajax section move is deprecated.', mdl: 'MDL-83562', )] protected function get_section_movedown_item(): ?link { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); $numsections = $this->format->get_last_section_number(); if ( $this->section->sectionnum == 0 || $this->section->sectionnum >= $numsections || $this->format->get_sectionid() || !has_capability('moodle/course:movesections', $this->coursecontext) ) { return null; } $url = new url( $this->baseurl, [ 'section' => $this->section->sectionnum, 'move' => 1, 'sesskey' => sesskey(), ] ); return new link_secondary( url: $url, icon: new pix_icon('i/down', ''), text: get_string('movedown'), attributes: [ // This tool disappears when the state is ready. 'class' => 'movedown whilenostate', ], ); } /** * Retrieves the permalink item for the section control menu. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_section_permalink_item(): ?link { if (!has_any_capability( [ 'moodle/course:movesections', 'moodle/course:update', 'moodle/course:sectionvisibility', ], $this->coursecontext ) ) { return null; } $url = new url( '/course/section.php', ['id' => $this->section->id] ); return new link_secondary( url: $url, icon: new pix_icon('i/link', ''), text: get_string('sectionlink', 'course'), attributes: [ 'class' => 'permalink', 'data-action' => 'permalink', ], ); } /** * Retrieves the delete item for the section control menu. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_section_delete_item(): ?link { if (!course_can_delete_section($this->format->get_course(), $this->section)) { return null; } $url = $this->format->get_update_url( action: 'section_delete', ids: [$this->section->id], returnurl: $this->baseurl, ); return new link_secondary( url: $url, icon: new pix_icon('i/delete', ''), text: get_string('delete'), attributes: [ 'class' => 'editing_delete text-danger', 'data-action' => 'deleteSection', 'data-id' => $this->section->id, ], ); } /** * Generate the edit control items of a section. * * It is not clear this kind of controls are still available in 4.0 so, for now, this * method is almost a clone of the previous section_control_items from the course/renderer.php. * * This method must remain public until the final deprecation of section_edit_control_items. * * @deprecated since Moodle 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return array of edit control items */ #[\core\attribute\deprecated( replacement: 'section_control_items', since: '5.0', mdl: 'MDL-83527', )] protected function section_control_items_legacy(): array { global $USER, $PAGE; \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); $format = $this->format; $section = $this->section; $course = $format->get_course(); $sectionreturn = !is_null($format->get_sectionid()) ? $format->get_sectionnum() : null; $user = $USER; $usecomponents = $format->supports_components(); $coursecontext = context_course::instance($course->id); $numsections = $format->get_last_section_number(); $isstealth = $section->is_orphan(); $baseurl = course_get_url($course, $sectionreturn); $baseurl->param('sesskey', sesskey()); $controls = []; // Only show the view link if we are not already in the section view page. if ($PAGE->pagetype !== 'course-view-section-' . $course->format) { $controls['view'] = [ 'url' => new url('/course/section.php', ['id' => $section->id]), 'icon' => 'i/viewsection', 'name' => get_string('view'), 'pixattr' => ['class' => ''], 'attr' => ['class' => 'icon view'], ]; } if (!$isstealth && has_capability('moodle/course:update', $coursecontext, $user)) { $params = ['id' => $section->id]; $params['sr'] = $section->section; if (get_string_manager()->string_exists('editsection', 'format_'.$format->get_format())) { $streditsection = get_string('editsection', 'format_'.$format->get_format()); } else { $streditsection = get_string('editsection'); } $controls['edit'] = [ 'url' => new url('/course/editsection.php', $params), 'icon' => 'i/settings', 'name' => $streditsection, 'pixattr' => ['class' => ''], 'attr' => ['class' => 'icon edit'], ]; if ($section->section) { $duplicatesectionurl = clone($baseurl); $duplicatesectionurl->param('sectionid', $section->id); $duplicatesectionurl->param('duplicatesection', 1); if (!is_null($sectionreturn)) { $duplicatesectionurl->param('sr', $sectionreturn); } $controls['duplicate'] = [ 'url' => $duplicatesectionurl, 'icon' => 't/copy', 'name' => get_string('duplicate'), 'pixattr' => ['class' => ''], 'attr' => ['class' => 'icon duplicate'], ]; } } if ($section->section) { $url = clone($baseurl); if (!is_null($sectionreturn)) { $url->param('sectionid', $format->get_sectionid()); } if (!$isstealth) { if (has_capability('moodle/course:sectionvisibility', $coursecontext, $user)) { $strhidefromothers = get_string('hidefromothers', 'format_' . $course->format); $strshowfromothers = get_string('showfromothers', 'format_' . $course->format); if ($section->visible) { // Show the hide/show eye. $url->param('hide', $section->section); $controls['visibility'] = [ 'url' => $url, 'icon' => 'i/show', 'name' => $strhidefromothers, 'pixattr' => ['class' => ''], 'attr' => [ 'class' => 'icon editing_showhide', 'data-sectionreturn' => $sectionreturn, 'data-action' => ($usecomponents) ? 'sectionHide' : 'hide', 'data-id' => $section->id, 'data-icon' => 'i/show', 'data-swapname' => $strshowfromothers, 'data-swapicon' => 'i/hide', ], ]; } else { $url->param('show', $section->section); $controls['visibility'] = [ 'url' => $url, 'icon' => 'i/hide', 'name' => $strshowfromothers, 'pixattr' => ['class' => ''], 'attr' => [ 'class' => 'icon editing_showhide', 'data-sectionreturn' => $sectionreturn, 'data-action' => ($usecomponents) ? 'sectionShow' : 'show', 'data-id' => $section->id, 'data-icon' => 'i/hide', 'data-swapname' => $strhidefromothers, 'data-swapicon' => 'i/show', ], ]; } } if (!$sectionreturn && has_capability('moodle/course:movesections', $coursecontext, $user)) { if ($usecomponents) { // This tool will appear only when the state is ready. $url = clone ($baseurl); $url->param('movesection', $section->section); $url->param('section', $section->section); $controls['movesection'] = [ 'url' => $url, 'icon' => 'i/dragdrop', 'name' => get_string('move', 'moodle'), 'pixattr' => ['class' => ''], 'attr' => [ 'class' => 'icon move waitstate', 'data-action' => 'moveSection', 'data-id' => $section->id, ], ]; } // Legacy move up and down links for non component-based formats. $url = clone($baseurl); if ($section->section > 1) { // Add a arrow to move section up. $url->param('section', $section->section); $url->param('move', -1); $strmoveup = get_string('moveup'); $controls['moveup'] = [ 'url' => $url, 'icon' => 'i/up', 'name' => $strmoveup, 'pixattr' => ['class' => ''], 'attr' => ['class' => 'icon moveup whilenostate'], ]; } $url = clone($baseurl); if ($section->section < $numsections) { // Add a arrow to move section down. $url->param('section', $section->section); $url->param('move', 1); $strmovedown = get_string('movedown'); $controls['movedown'] = [ 'url' => $url, 'icon' => 'i/down', 'name' => $strmovedown, 'pixattr' => ['class' => ''], 'attr' => ['class' => 'icon movedown whilenostate'], ]; } } } if (course_can_delete_section($course, $section)) { if (get_string_manager()->string_exists('deletesection', 'format_'.$course->format)) { $strdelete = get_string('deletesection', 'format_'.$course->format); } else { $strdelete = get_string('deletesection'); } $params = [ 'id' => $section->id, 'delete' => 1, 'sesskey' => sesskey(), ]; if (!is_null($sectionreturn)) { $params['sr'] = $sectionreturn; } $url = new url( '/course/editsection.php', $params, ); $controls['delete'] = [ 'url' => $url, 'icon' => 'i/delete', 'name' => $strdelete, 'pixattr' => ['class' => ''], 'attr' => [ 'class' => 'icon editing_delete text-danger', 'data-action' => 'deleteSection', 'data-id' => $section->id, ], ]; } } if ( !$isstealth && has_any_capability([ 'moodle/course:movesections', 'moodle/course:update', 'moodle/course:sectionvisibility', ], $coursecontext) ) { $sectionlink = new url( '/course/section.php', ['id' => $section->id] ); $controls['permalink'] = [ 'url' => $sectionlink, 'icon' => 'i/link', 'name' => get_string('sectionlink', 'course'), 'pixattr' => ['class' => ''], 'attr' => [ 'class' => 'icon', 'data-action' => 'permalink', ], ]; } return $controls; } } PK$9\)4646/format/classes/output/local/content/section.phpnu[. /** * Contains the default section course format output class. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content; use context_course; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; use renderer_base; use section_info; use stdClass; /** * Base class to render a course section. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class section implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format */ protected $format; /** @var section_info the section info */ protected $section; /** @var section header output class */ protected $headerclass; /** @var cm list output class */ protected $cmlistclass; /** @var section summary output class */ protected $summaryclass; /** @var activities summary output class */ protected $cmsummaryclass; /** @var section control menu output class */ protected $controlclass; /** @var section availability output class */ protected $availabilityclass; /** @var optional visibility output class */ protected $visibilityclass; /** @var bool if the title is hidden for some reason */ protected $hidetitle = false; /** @var bool if the title is hidden for some reason */ protected $hidecontrols = false; /** @var bool if the section is considered stealth */ protected $isstealth = false; /** @var string control menu class. */ protected $controlmenuclass; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info */ public function __construct(course_format $format, section_info $section) { $this->format = $format; $this->section = $section; $this->isstealth = $section->is_orphan(); // Load output classes names from format. $this->headerclass = $format->get_output_classname('content\\section\\header'); $this->cmlistclass = $format->get_output_classname('content\\section\\cmlist'); $this->summaryclass = $format->get_output_classname('content\\section\\summary'); $this->cmsummaryclass = $format->get_output_classname('content\\section\\cmsummary'); $this->controlmenuclass = $format->get_output_classname('content\\section\\controlmenu'); $this->availabilityclass = $format->get_output_classname('content\\section\\availability'); $this->visibilityclass = $format->get_output_classname('content\\section\\visibility'); } /** * Check if the section is considered stealth. * * @return bool */ public function is_stealth(): bool { return $this->isstealth; } /** * Hide the section title. * * This is used on blocks or in the home page where an isolated section is displayed. */ public function hide_title(): void { $this->hidetitle = true; } /** * Hide the section controls. * * This is used on blocks or in the home page where an isolated section is displayed. */ public function hide_controls(): void { $this->hidecontrols = true; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(renderer_base $output): stdClass { global $USER, $PAGE; $format = $this->format; $course = $format->get_course(); $section = $this->section; $summary = new $this->summaryclass($format, $section); $data = (object)[ 'num' => $section->section ?? '0', 'id' => $section->id, 'sectionreturnnum' => $format->get_sectionnum(), 'insertafter' => false, 'summary' => $summary->export_for_template($output), 'highlightedlabel' => $format->get_section_highlighted_name(), 'sitehome' => $course->id == SITEID, 'editing' => $PAGE->user_is_editing(), 'displayonesection' => ($course->id != SITEID && $format->get_sectionid() == $section->id), // Section name is used as data attribute is to facilitate behat locators. 'sectionname' => $format->get_section_name($section), ]; $haspartials = []; $haspartials['availability'] = $this->add_availability_data($data, $output); $haspartials['visibility'] = $this->add_visibility_data($data, $output); $haspartials['editor'] = $this->add_editor_data($data, $output); $haspartials['header'] = $this->add_header_data($data, $output); $haspartials['cm'] = $this->add_cm_data($data, $output); $this->add_format_data($data, $haspartials, $output); return $data; } /** * Add the section header to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool if the cm has name data */ protected function add_header_data(stdClass &$data, renderer_base $output): bool { if (!empty($this->hidetitle)) { return false; } $section = $this->section; $format = $this->format; $header = new $this->headerclass($format, $section); $headerdata = $header->export_for_template($output); // When a section is displayed alone the title goes over the section, not inside it. if ($section->section != 0 && $section->section == $format->get_sectionnum()) { $data->singleheader = $headerdata; } else { $data->header = $headerdata; } return true; } /** * Add the section cm list to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool if the cm has name data */ protected function add_cm_data(stdClass &$data, renderer_base $output): bool { $result = false; $section = $this->section; $format = $this->format; $showsummary = ($section->section != 0 && $section->section != $format->get_sectionnum() && $format->get_course_display() == COURSE_DISPLAY_MULTIPAGE && !$format->show_editor() ); $showcmlist = $section->uservisible; // Add activities summary if necessary. if ($showsummary) { $cmsummary = new $this->cmsummaryclass($format, $section); $data->cmsummary = $cmsummary->export_for_template($output); $data->onlysummary = true; $result = true; if (!$format->is_section_current($section)) { // In multipage, only the current section (and the section zero) has elements. $showcmlist = false; } } // Add the cm list. if ($showcmlist) { $cmlist = new $this->cmlistclass($format, $section); $data->cmlist = $cmlist->export_for_template($output); $result = true; } return $result; } /** * Add the section availability to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool if the cm has name data */ protected function add_availability_data(stdClass &$data, renderer_base $output): bool { $availability = new $this->availabilityclass($this->format, $this->section); $data->availability = $availability->export_for_template($output); $data->restrictionlock = !empty($this->section->availableinfo); $data->hasavailability = $availability->has_availability($output); return true; } /** * Add the section vibility information to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool if the cm has name data */ protected function add_visibility_data(stdClass &$data, renderer_base $output): bool { global $USER; $result = false; // Check if it is a stealth sections (orphaned). if ($this->isstealth) { $data->isstealth = true; $data->ishidden = true; $result = true; } if (!$this->section->visible) { $data->ishidden = true; $course = $this->format->get_course(); $context = context_course::instance($course->id); if (has_capability('moodle/course:viewhiddensections', $context, $USER)) { $result = true; } } /* @var \core_courseformat\output\local\content\section\visibility $visibility By default the visibility class used * here but can be overriden by any course format */ $visibility = new $this->visibilityclass($this->format, $this->section); $data->visibility = $visibility->export_for_template($output); return $result; } /** * Add the section editor attributes to the data structure. * * @param stdClass $data the current cm data reference * @param renderer_base $output typically, the renderer that's calling this function * @return bool if the cm has name data */ protected function add_editor_data(stdClass &$data, renderer_base $output): bool { $course = $this->format->get_course(); $coursecontext = context_course::instance($course->id); $editcaps = []; if (has_capability('moodle/course:sectionvisibility', $coursecontext)) { $editcaps = ['moodle/course:sectionvisibility']; } if (!$this->format->show_editor($editcaps)) { return false; } // In a single section page the control menu is located in the page header. if (empty($this->hidecontrols) && $this->format->get_sectionid() != $this->section->id) { $controlmenu = new $this->controlmenuclass($this->format, $this->section); $data->controlmenu = $controlmenu->export_for_template($output); } if (!$this->isstealth) { $data->cmcontrols = $output->course_section_add_cm_control( $course, $this->section->section, $this->format->get_sectionnum() ); } return true; } /** * Add the section format attributes to the data structure. * * @param stdClass $data the current cm data reference * @param bool[] $haspartials the result of loading partial data elements * @param renderer_base $output typically, the renderer that's calling this function * @return bool if the cm has name data */ protected function add_format_data(stdClass &$data, array $haspartials, renderer_base $output): bool { $section = $this->section; $format = $this->format; $data->iscoursedisplaymultipage = ($format->get_course_display() == COURSE_DISPLAY_MULTIPAGE); if ($data->num === 0 && !$data->iscoursedisplaymultipage) { $data->collapsemenu = true; } $data->contentcollapsed = $this->is_section_collapsed(); if ($format->is_section_current($section)) { $data->iscurrent = true; $data->currentlink = get_accesshide( get_string('currentsection', 'format_' . $format->get_format()) ); } return true; } /** * Returns true if the current section should be shown collapsed. * * @return bool */ protected function is_section_collapsed(): bool { global $PAGE; $contentcollapsed = false; $preferences = $this->format->get_sections_preferences(); if (isset($preferences[$this->section->id])) { $sectionpreferences = $preferences[$this->section->id]; if (!empty($sectionpreferences->contentcollapsed)) { $contentcollapsed = true; } } // No matter if the user's preference was to collapse the section or not: If the // 'expandsection' parameter has been specified, it will be shown uncollapsed. $expandsection = $PAGE->url->get_param('expandsection'); if ($expandsection !== null && $this->section->section == $expandsection) { $contentcollapsed = false; } return $contentcollapsed; } } PK$9\Ooo7format/classes/output/local/content/cm/availability.phpnu[. /** * Contains the default activity availability information. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content\cm; use core_courseformat\output\local\content\section\availability as section_avalability; use cm_info; use core_courseformat\base as course_format; use section_info; use stdClass; use core_availability\info_module; use core_availability\info; /** * Base class to render a course module availability inside a course format. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class availability extends section_avalability { /** @var course_format the course format */ protected $format; /** @var section_info the section object */ protected $section; /** @var cm_info the course module instance */ protected $mod; /** @var array optional display options */ protected $displayoptions; /** @var bool the has availability attribute name */ protected $hasavailabilityname; /** @var stdClass|null the instance export data */ protected $data = null; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info * @param cm_info $mod the course module ionfo * @param array $displayoptions optional extra display options */ public function __construct(course_format $format, section_info $section, cm_info $mod, array $displayoptions = []) { $this->format = $format; $this->section = $section; $this->mod = $mod; $this->displayoptions = $displayoptions; $this->hasavailabilityname = 'hasmodavailability'; } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass|null data context for a mustache template */ public function export_for_template(\renderer_base $output): ?stdClass { if (!$this->format->show_activity_editor_options($this->mod)) { return null; } return parent::export_for_template($output); } /** * Get the availability data to be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function * @return array the availability data. */ protected function get_info(\renderer_base $output): array { if (!$this->mod->is_visible_on_course_page()) { // Nothing to be displayed to the user. return []; } if (!$this->mod->uservisible) { return ['info' => $this->user_availability_info($output)]; } $editurl = new \moodle_url( '/course/modedit.php', ['update' => $this->mod->id, 'showonly' => 'availabilityconditionsheader'] ); return ['editurl' => $editurl->out(false), 'info' => $this->conditional_availability_info($output)]; } /** * Get the current user availability data. * * This is a student who is not allowed to see the module but might be allowed * to see availability info (i.e. "Available from ..."). * * @param \renderer_base $output typically, the renderer that's calling this function * @return array the availability data. */ protected function user_availability_info(\renderer_base $output): array { if (empty($this->mod->availableinfo)) { return []; } $info = []; $info[] = $this->get_availability_data($output, $this->mod->availableinfo, 'isrestricted'); return $info; } /** * Get the activity availability data to display. * * @param \renderer_base $output typically, the renderer that's calling this function * @return array the availability data. */ protected function conditional_availability_info(\renderer_base $output): array { global $CFG; // This is a teacher who is allowed to see module but still should see the // information that module is not available to all/some students. $mod = $this->mod; $modcontext = $mod->context; $canviewhidden = has_capability('moodle/course:viewhiddenactivities', $modcontext); if (!$canviewhidden || empty($CFG->enableavailability)) { return []; } // Display information about conditional availability. // Don't add availability information if user is not editing and activity is hidden. if (!$mod->visible && !$this->format->show_editor()) { return []; } $ci = new info_module($mod); $fullinfo = $ci->get_full_information(); if (!$fullinfo) { return []; } $info = []; $hidinfoclass = 'isrestricted isfullinfo'; if (!$mod->visible) { $hidinfoclass .= ' hide'; } $info[] = $this->get_availability_data($output, $fullinfo, $hidinfoclass); return $info; } } PK$9\)5format/classes/output/local/content/cm/completion.phpnu[. namespace core_courseformat\output\local\content\cm; use cm_info; use core_course\output\activity_completion; use section_info; use renderable; use stdClass; use core\output\named_templatable; use core\output\local\dropdown\dialog as dropdown_dialog; use core_completion\cm_completion_details; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; /** * Base class to render course module completion. * * @package core_courseformat * @copyright 2023 Mikel Martin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion implements named_templatable, renderable { use courseformat_named_templatable; /** @var bool $smallbutton if the button is rendered small (like in course page). */ private bool $smallbutton = true; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info * @param cm_info $mod the course module ionfo */ public function __construct( protected course_format $format, protected section_info $section, protected cm_info $mod, ) { } /** * Set if the button is rendered small. * * By default, the button is rendered small like in the course page. However, * in other pages like the activities overview, the button is rendered like * a regular button. * * @param bool $smallbutton if the button is rendered small (like in course page). */ public function set_smallbutton(bool $smallbutton): void { $this->smallbutton = $smallbutton; } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass|null data context for a mustache template */ public function export_for_template(\renderer_base $output): ?stdClass { global $USER; if (!$this->format->show_activity_editor_options($this->mod)) { return null; } $course = $this->mod->get_course(); $showcompletionconditions = $course->showcompletionconditions == COMPLETION_SHOW_CONDITIONS; $completiondetails = cm_completion_details::get_instance($this->mod, $USER->id, $showcompletionconditions); $showcompletioninfo = $completiondetails->has_completion() && ($showcompletionconditions || $completiondetails->show_manual_completion()); if (!$showcompletioninfo) { return null; } $completion = new activity_completion($this->mod, $completiondetails, $this->smallbutton); $completiondata = $completion->export_for_template($output); if ($completiondata->isautomatic || ($completiondata->ismanual && !$completiondata->istrackeduser)) { $completiondata->completiondialog = $this->get_completion_dialog($output, $completiondata); } return $completiondata; } /** * Get the completion dialog. * * @param \renderer_base $output typically, the renderer that's calling this function * @param stdClass $completioninfo the completion info * @return array the completion dialog exported for template */ protected function get_completion_dialog(\renderer_base $output, stdClass $completioninfo): array { global $PAGE; $editurl = new \moodle_url( '/course/modedit.php', ['update' => $this->mod->id, 'showonly' => 'activitycompletionheader'] ); $completioninfo->editurl = $editurl->out(false); $completioninfo->editing = $PAGE->user_is_editing(); $completioninfo->hasconditions = $completioninfo->ismanual || count($completioninfo->completiondetails) > 0; $dialogcontent = $output->render_from_template('core_courseformat/local/content/cm/completion_dialog', $completioninfo); $buttoncontent = get_string('completionmenuitem', 'completion'); $buttonclass = 'btn-subtle-body'; if ($completioninfo->istrackeduser) { $buttoncontent = get_string('todo', 'completion'); if ($completioninfo->overallcomplete) { $buttoncontent = $output->pix_icon('i/checked', '') . " " . get_string('completion_manual:done', 'core_course'); $buttonclass = 'btn-subtle-success'; } } $buttonclass .= $this->smallbutton ? ' btn-sm' : ''; $completiondialog = new dropdown_dialog($buttoncontent, $dialogcontent, [ 'classes' => 'completion-dropdown', 'buttonclasses' => 'btn dropdown-toggle icon-no-margin ' . $buttonclass, 'dropdownposition' => dropdown_dialog::POSITION['end'], ]); return $completiondialog->export_for_template($output); } } PK$9\xᦙ4format/classes/output/local/content/cm/groupmode.phpnu[. namespace core_courseformat\output\local\content\cm; use cm_info; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use core\output\named_templatable; use core\output\choicelist; use core\output\local\dropdown\status; use pix_icon; use renderable; use section_info; use stdClass; /** * Base class to render an activity group mode badge. * * @package core_courseformat * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class groupmode implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format */ protected $format; /** @var section_info the section object */ protected $section; /** @var cm_info the course module instance */ protected $mod; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info * @param cm_info $mod the course module ionfo */ public function __construct( course_format $format, section_info $section, cm_info $mod, ) { $this->format = $format; $this->section = $section; $this->mod = $mod; } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass|null data context for a mustache template */ public function export_for_template(\renderer_base $output): ?stdClass { if (!$this->format->show_groupmode($this->mod)) { return null; } $usecomponents = $this->format->supports_components(); if ($this->format->show_editor() && $usecomponents && !$this->mod->coursegroupmodeforce) { return $this->build_editor_data($output); } // If the group mode is not editable, the no groups badge is not displayed. if ($this->mod->effectivegroupmode === NOGROUPS) { return null; } return $this->build_static_data($output); } /** * Build the data for the static badge. * @param \renderer_base $output * @return stdClass */ protected function build_static_data(\renderer_base $output): stdClass { switch ($this->mod->effectivegroupmode) { case SEPARATEGROUPS: $groupalt = get_string('groupsseparate', 'group'); $groupicon = $this->get_action_icon('cmSeparateGroups', $groupalt); break; case VISIBLEGROUPS: $groupalt = get_string('groupsvisible', 'group'); $groupicon = $this->get_action_icon('cmVisibleGroups', $groupalt); break; case NOGROUPS: default: $groupalt = get_string('groupsnone', 'group'); $groupicon = $this->get_action_icon('cmNoGroups', $groupalt); break; } $data = (object) [ 'groupicon' => $output->render($groupicon), 'groupalt' => $groupalt, 'isInteractive' => false, ]; return $data; } /** * Build the data for the interactive dropdown. * @param \renderer_base $output * @return stdClass */ protected function build_editor_data(\renderer_base $output): stdClass { $choice = $this->get_choice_list(); $result = $this->get_dropdown_data($output, $choice); $result->autohide = ($this->mod->effectivegroupmode === NOGROUPS); return $result; } /** * Build the data for the interactive dropdown. * @param \renderer_base $output * @param choicelist $choice the choice list * @return stdClass */ protected function get_dropdown_data(\renderer_base $output, choicelist $choice): stdClass { $buttondata = $this->build_static_data($output); $dropdown = new status( $buttondata->groupicon, $choice, ['dialogwidth' => status::WIDTH['big']], ); $dropdown->set_dialog_width(status::WIDTH['small']); $dropdown->set_position(status::POSITION['end']); return (object) [ 'isInteractive' => true, 'groupicon' => $buttondata->groupicon, 'groupalt' => $buttondata->groupalt, 'dropwdown' => $dropdown->export_for_template($output), ]; } /** * Create a choice list for the dropdown. * @return choicelist the choice list */ public function get_choice_list(): choicelist { $choice = new choicelist(); $choice->add_option( NOGROUPS, get_string('groupsnone', 'group'), $this->get_option_data(null, 'cmNoGroups', 'cm_nogroups') ); $choice->add_option( SEPARATEGROUPS, get_string('groupsseparate', 'group'), $this->get_option_data('groupsseparate', 'cmSeparateGroups', 'cm_separategroups') ); $choice->add_option( VISIBLEGROUPS, get_string('groupsvisible', 'group'), $this->get_option_data('groupsvisible', 'cmVisibleGroups', 'cm_visiblegroups') ); $choice->set_selected_value($this->mod->effectivegroupmode); return $choice; } /** * Get the data for the option. * @param string|null $name the name of the option * @param string $mutation the mutation name * @param string $stateaction the state action name * @return array */ private function get_option_data(?string $name, string $mutation, string $stateaction): array { $format = $this->format; $nonajaxurl = $format->get_update_url( action: $stateaction, ids: [$this->mod->id], returnurl: $format->get_view_url($format->get_sectionnum(), ['navigation' => true]), ); return [ 'description' => ($name) ? get_string("groupmode_{$name}_help", 'group') : null, // The dropdown icons are decorative, so we don't need to provide alt text. 'icon' => $this->get_action_icon($mutation), 'url' => $nonajaxurl, 'extras' => [ 'data-id' => $this->mod->id, 'data-action' => $mutation, ] ]; } /** * Get the group mode icon. * @param string $groupmode the group mode * @param string $groupalt the alt text * @return pix_icon */ protected function get_action_icon(string $groupmode, string $groupalt = ''): pix_icon { $icons = [ 'cmNoGroups' => 'i/groupn', 'cmSeparateGroups' => 'i/groups', 'cmVisibleGroups' => 'i/groupv', ]; return new pix_icon($icons[$groupmode], $groupalt); } } PK$9\x͜1format/classes/output/local/content/cm/cmname.phpnu[. /** * Contains the default activity name inplace editable. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content\cm; use cm_info; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; use section_info; use stdClass; /** * Base class to render a course module inplace editable header. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cmname implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format */ protected $format; /** @var section_info the section object */ protected $section; /** @var cm_info the course module instance */ protected $mod; /** @var array optional display options */ protected $displayoptions; /** @var string the activity title output class name */ protected $titleclass; /** @var string the activity icon output class name */ protected $iconclass; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info * @param cm_info $mod the course module ionfo * @param null $unused This parameter has been deprecated since 4.1 and should not be used anymore. * @param array $displayoptions optional extra display options */ public function __construct( course_format $format, section_info $section, cm_info $mod, ?bool $unused = null, array $displayoptions = [] ) { if ($unused !== null) { debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER); } $this->format = $format; $this->section = $section; $this->mod = $mod; $this->displayoptions = $displayoptions; // Get the necessary classes. $this->titleclass = $format->get_output_classname('content\\cm\\title'); $this->iconclass = $format->get_output_classname('content\\cm\\cmicon'); } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output): array { $mod = $this->mod; $displayoptions = $this->displayoptions; if (!$this->has_name()) { // Nothing to be displayed to the user. return []; } $data = [ 'url' => $mod->url, 'modname' => $mod->modname, 'textclasses' => $displayoptions['textclasses'] ?? '', 'activityicon' => $this->get_icon_data($output), 'activityname' => $this->get_title_data($output), ]; return $data; } /** * Get the title data. * * @param \renderer_base $output typically, the renderer that's calling this function * @return array data context for a mustache template */ protected function get_title_data(\renderer_base $output): array { $title = new $this->titleclass( $this->format, $this->section, $this->mod, $this->displayoptions ); return (array) $title->export_for_template($output); } /** * Get the icon data. * * @param \renderer_base $output typically, the renderer that's calling this function * @return array data context for a mustache template */ protected function get_icon_data(\renderer_base $output): array { $icon = new $this->iconclass( $this->format, $this->mod, ); return (array) $icon->export_for_template($output); } /** * Return if the activity has a visible name. * * @return bool if the title is visible. */ public function has_name(): bool { return $this->mod->is_visible_on_course_page() && $this->mod->url; } } PK$9\`vTT0format/classes/output/local/content/cm/title.phpnu[. /** * Contains the default activity title. * * This class is usually rendered inside the cmname inplace editable. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content\cm; use cm_info; use core\output\inplace_editable; use core\output\named_templatable; use core_courseformat\base as course_format; use core_text; use lang_string; use renderable; use section_info; use stdClass; use core_external\external_api; use context_module; /** * Base class to render a course module title inside a course format. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class title extends inplace_editable implements named_templatable, renderable { /** @var course_format the course format */ protected $format; /** @var section_info the section object */ protected $section; /** @var cm_info the course module instance */ protected $mod; /** @var array optional display options */ protected $displayoptions; /** @var editable if the title is editable */ protected $editable; /** @var displaytemplate the default display template */ protected $displaytemplate = 'core_courseformat/local/content/cm/title'; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info * @param cm_info $mod the course module ionfo * @param array $displayoptions optional extra display options * @param bool|null $editable force editable value */ public function __construct( course_format $format, section_info $section, cm_info $mod, array $displayoptions = [], ?bool $editable = null ) { $this->format = $format; $this->section = $section; $this->mod = $mod; // Usually displayoptions are loaded in the main cm output. However when the user uses the inplace editor // the cmname output does not calculate the css classes. $this->displayoptions = $this->load_display_options($displayoptions); if ($editable === null) { $editable = $format->show_editor(); } $this->editable = $editable; // Setup inplace editable. parent::__construct( 'core_course', 'activityname', $mod->id, $this->editable, $mod->name, $mod->name, new lang_string('edittitle'), new lang_string('newactivityname', '', $mod->get_formatted_name()) ); } /** * Get the name of the template to use for this templatable. * * @param \renderer_base $renderer The renderer requesting the template name * @return string */ public function get_template_name(\renderer_base $renderer): string { return 'core/inplace_editable'; } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output): array { // Inplace editable uses pre-rendered elements and does not allow line beaks in the UI value. $this->displayvalue = str_replace("\n", "", $this->get_title_displayvalue()); if (trim($this->displayvalue) == '') { $this->editable = false; } return parent::export_for_template($output); } /** * Return the title template data to be used inside the inplace editable. * */ protected function get_title_displayvalue(): string { global $PAGE; // Inplace editable uses core renderer by default. However, course elements require // the format specific renderer. $courseoutput = $this->format->get_renderer($PAGE); $mod = $this->mod; $data = (object)[ 'url' => $mod->url, 'instancename' => $mod->get_formatted_name(), 'uservisible' => $mod->uservisible, 'linkclasses' => $this->displayoptions['linkclasses'], ]; // File type after name, for alphabetic lists (screen reader). if (strpos( core_text::strtolower($data->instancename), core_text::strtolower($mod->modfullname) ) === false) { $data->altname = get_accesshide(' ' . $mod->modfullname); } // Get on-click attribute value if specified and decode the onclick - it // has already been encoded for display (puke). $data->onclick = htmlspecialchars_decode($mod->onclick, ENT_QUOTES); return $courseoutput->render_from_template( $this->displaytemplate, $data ); } /** * Load the required display options if not present already. * * In most cases, display options are provided as a param when creating the * object. However, inplace_editable and some blocks does not know all of them as it is * called in a webservice and we need to ensure it is calculated. * * @param array $displayoptions the provided dispaly options * @return array the full display options list */ protected function load_display_options(array $displayoptions): array { $format = $this->format; $mod = $this->mod; if ( isset($displayoptions['linkclasses']) && isset($displayoptions['textclasses']) && isset($displayoptions['onclick']) ) { return $displayoptions; } $cmclass = $format->get_output_classname('content\\cm'); $cmoutput = new $cmclass( $format, $this->section, $mod, $displayoptions ); $displayoptions['linkclasses'] = $cmoutput->get_link_classes(); $displayoptions['textclasses'] = $cmoutput->get_text_classes(); $displayoptions['onclick'] = $cmoutput->get_onclick_code(); return $displayoptions; } /** * Updates course module name. * * This method is used mainly by inplace_editable webservice. * * @param int $itemid course module id * @param string $newvalue new name * @return static */ public static function update($itemid, $newvalue) { $context = context_module::instance($itemid); // Check access. external_api::validate_context($context); require_capability('moodle/course:manageactivities', $context); // Trim module name and Update value. set_coursemodule_name($itemid, trim($newvalue)); $coursemodulerecord = get_coursemodule_from_id('', $itemid, 0, false, MUST_EXIST); // Return instance. $modinfo = get_fast_modinfo($coursemodulerecord->course); $cm = $modinfo->get_cm($itemid); $section = $modinfo->get_section_info($cm->sectionnum); $format = course_get_format($cm->course); return new static($format, $section, $cm, [], true); } } PK$9\mCC?format/classes/output/local/content/cm/delegatedcontrolmenu.phpnu[. namespace core_courseformat\output\local\content\cm; use cm_info; use core\context\course as context_course; use core\context\module as context_module; use core\output\action_menu; use core\output\action_menu\link; use core\output\action_menu\link_secondary; use core\output\renderer_base; use core_courseformat\base as course_format; use core_courseformat\output\local\content\basecontrolmenu; use core\output\pix_icon; use core\url; use section_info; /** * Base class to render delegated section controls. * * @package core_courseformat * @copyright 2024 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delegatedcontrolmenu extends basecontrolmenu { /** @var context_module|null modcontext the module context if any */ protected ?context_module $modcontext = null; /** @var bool $canmanageactivities Optimization to know if the user can manage activities */ protected bool $canmanageactivities; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info * @param cm_info $mod the module info */ public function __construct(course_format $format, section_info $section, cm_info $mod) { parent::__construct($format, $section, $mod, $section->id); $this->modcontext = context_module::instance($mod->id); $this->canmanageactivities = has_capability('moodle/course:manageactivities', $this->modcontext); } /** * Generate the default delegated section action menu. * * This method is public in case some block needs to modify the menu before output it. * * @param renderer_base $output typically, the renderer that's calling this function * @return action_menu|null the action menu */ public function get_default_action_menu(renderer_base $output): ?action_menu { $controls = $this->delegated_control_items(); return $this->format_controls($controls); } /** * Generate the edit control items of a section. * * @return array of edit control items */ public function delegated_control_items() { // TODO remove this if as part of MDL-83530. if (!$this->format->supports_components()) { return $this->delegated_control_items_legacy(); } $controls = []; $controls['view'] = $this->get_section_view_item(); $controls['edit'] = $this->get_section_edit_item(); $controls['visibility'] = $this->get_section_visibility_item(); $controls['movesection'] = $this->get_cm_move_item(); $controls['permalink'] = $this->get_section_permalink_item(); $controls['delete'] = $this->get_cm_delete_item(); return $controls; } /** * Retrieves the view item for the section control menu. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_section_view_item(): ?link { // Only show the view link if we are not already in the section view page. if ($this->format->get_sectionid() == $this->section->id) { return null; } return new link_secondary( url: new url('/course/section.php', ['id' => $this->section->id]), icon: new pix_icon('i/viewsection', ''), text: get_string('view'), attributes: ['class' => 'view'], ); } /** * Retrieves the edit item for the section control menu. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_section_edit_item(): ?link { if (!has_capability('moodle/course:update', $this->coursecontext)) { return null; } $url = new url( '/course/editsection.php', [ 'id' => $this->section->id, 'sr' => $this->section->sectionnum, ] ); return new link_secondary( url: $url, icon: new pix_icon('i/settings', ''), text: get_string('editsection'), attributes: ['class' => 'edit'], ); } /** * Generates the move item for a course module. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_cm_move_item(): ?link { // Only show the move link if we are not already in the section view page. if ( !$this->canmanageactivities || $this->format->get_sectionid() == $this->section->id ) { return null; } // The move action uses visual elements on the course page. $url = new url('/course/mod.php', ['sesskey' => sesskey()]); $sectionnumreturn = $this->format->get_sectionnum(); if ($sectionnumreturn !== null) { $url->param('sr', $sectionnumreturn); } return new link_secondary( url: $url, icon: new pix_icon('i/dragdrop', ''), text: get_string('move'), attributes: [ // This tool requires ajax and will appear only when the frontend state is ready. 'class' => 'editing_movecm waitstate', 'data-action' => 'moveCm', 'data-id' => $this->mod->id, ], ); } /** * Retrieves the get_section_visibility_menu_item item for the section control menu. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_section_visibility_item(): ?link { // To avoid exponential complexity, we only allow subsection visibility actions // when the parent section is visible. $parentsection = $this->mod->get_section_info(); if ( $this->section->sectionnum == 0 || !$parentsection->visible || !has_capability('moodle/course:sectionvisibility', $this->coursecontext) || !has_capability('moodle/course:activityvisibility', $this->modcontext) ) { return null; } $sectionreturn = $this->format->get_sectionnum(); $strhide = get_string('hide'); $strshow = get_string('show'); if ($this->section->visible) { $action = 'section_hide'; $icon = 'i/show'; $name = $strhide; $attributes = [ 'class' => 'icon editing_showhide', 'data-sectionreturn' => $sectionreturn, 'data-action' => 'sectionHide', 'data-id' => $this->section->id, 'data-icon' => 'i/show', 'data-swapname' => $strshow, 'data-swapicon' => 'i/hide', ]; } else { $action = 'section_show'; $icon = 'i/hide'; $name = $strshow; $attributes = [ 'class' => 'editing_showhide', 'data-sectionreturn' => $sectionreturn, 'data-action' => 'sectionShow', 'data-id' => $this->section->id, 'data-icon' => 'i/hide', 'data-swapname' => $strhide, 'data-swapicon' => 'i/show', ]; } $url = $this->format->get_update_url( action: $action, ids: [$this->section->id], returnurl: $this->baseurl, ); return new link_secondary( url: $url, icon: new pix_icon($icon, ''), text: $name, attributes: $attributes, ); } /** * Retrieves the permalink item for the section control menu. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_section_permalink_item(): ?link { if (!has_any_capability( [ 'moodle/course:movesections', 'moodle/course:update', 'moodle/course:sectionvisibility', ], $this->coursecontext ) ) { return null; } $url = new url( '/course/section.php', ['id' => $this->section->id] ); return new link_secondary( url: $url, icon: new pix_icon('i/link', ''), text: get_string('sectionlink', 'course'), attributes: [ 'class' => 'permalink', 'data-action' => 'permalink', ], ); } /** * Generates the delete item for a course module. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_cm_delete_item(): ?link { if (!$this->canmanageactivities) { return null; } $url = $this->format->get_update_url( action: 'cm_delete', ids: [$this->mod->id], returnurl: $this->baseurl, ); return new link_secondary( url: $url, icon: new pix_icon('t/delete', ''), text: get_string('delete'), attributes: [ 'class' => 'editing_delete text-danger', 'data-action' => 'cmDelete', 'data-sectionreturn' => $this->format->get_sectionnum(), 'data-id' => $this->mod->id, ], ); } /** * Generate the edit control items of a section. * * It is not clear this kind of controls are still available in 4.0 so, for now, this * method is almost a clone of the previous section_control_items from the course/renderer.php. * * @deprecated since Moodle 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return array of edit control items */ #[\core\attribute\deprecated( replacement: 'delegated_control_items', since: '5.0', mdl: 'MDL-83527', )] protected function delegated_control_items_legacy(): array { global $USER; \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); $format = $this->format; $section = $this->section; $cm = $this->mod; $course = $format->get_course(); $sectionreturn = !is_null($format->get_sectionid()) ? $format->get_sectionnum() : null; $user = $USER; $usecomponents = $format->supports_components(); $coursecontext = context_course::instance($course->id); $baseurl = course_get_url($course, $sectionreturn); $baseurl->param('sesskey', sesskey()); $cmbaseurl = new url('/course/mod.php'); $cmbaseurl->param('sesskey', sesskey()); $hasmanageactivities = has_capability('moodle/course:manageactivities', $coursecontext); $isheadersection = $format->get_sectionid() == $section->id; $controls = []; // Only show the view link if we are not already in the section view page. if (!$isheadersection) { $controls['view'] = [ 'url' => new url('/course/section.php', ['id' => $section->id]), 'icon' => 'i/viewsection', 'name' => get_string('view'), 'pixattr' => ['class' => ''], 'attr' => ['class' => 'view'], ]; } if (has_capability('moodle/course:update', $coursecontext, $user)) { $params = ['id' => $section->id]; $params['sr'] = $section->section; if (get_string_manager()->string_exists('editsection', 'format_'.$format->get_format())) { $streditsection = get_string('editsection', 'format_'.$format->get_format()); } else { $streditsection = get_string('editsection'); } // Edit settings goes to section settings form. $controls['edit'] = [ 'url' => new url('/course/editsection.php', $params), 'icon' => 'i/settings', 'name' => $streditsection, 'pixattr' => ['class' => ''], 'attr' => ['class' => 'edit'], ]; } // Hide/Show uses module functionality. // Hide/Show options will be available for subsections inside visible sections only. $parentsection = $cm->get_section_info(); $availablevisibility = has_capability('moodle/course:sectionvisibility', $coursecontext, $user) && $parentsection->visible; if ($availablevisibility) { $url = clone($baseurl); if (!is_null($sectionreturn)) { $url->param('sr', $format->get_sectionid()); } $strhidefromothers = get_string('hidefromothers', 'format_' . $course->format); $strshowfromothers = get_string('showfromothers', 'format_' . $course->format); if ($section->visible) { // Show the hide/show eye. $url->param('hide', $section->section); $controls['visiblity'] = [ 'url' => $url, 'icon' => 'i/show', 'name' => $strhidefromothers, 'pixattr' => ['class' => ''], 'attr' => [ 'class' => 'editing_showhide', 'data-sectionreturn' => $sectionreturn, 'data-action' => ($usecomponents) ? 'sectionHide' : 'hide', 'data-id' => $section->id, 'data-swapname' => $strshowfromothers, 'data-swapicon' => 'i/show', ], ]; } else { $url->param('show', $section->section); $controls['visiblity'] = [ 'url' => $url, 'icon' => 'i/hide', 'name' => $strshowfromothers, 'pixattr' => ['class' => ''], 'attr' => [ 'class' => 'editing_showhide', 'data-sectionreturn' => $sectionreturn, 'data-action' => ($usecomponents) ? 'sectionShow' : 'show', 'data-id' => $section->id, 'data-swapname' => $strhidefromothers, 'data-swapicon' => 'i/hide', ], ]; } } // Only show the move link if we are not already in the section view page. // Move (only for component compatible formats). if (!$isheadersection && $hasmanageactivities && $usecomponents) { $controls['move'] = [ 'url' => new url('/course/mod.php', ['copy' => $cm->id]), 'icon' => 'i/dragdrop', 'name' => get_string('move'), 'pixattr' => ['class' => ''], 'attr' => [ 'class' => 'editing_movecm ', 'data-action' => 'moveCm', 'data-id' => $cm->id, ], ]; } // Delete deletes the module. if ($hasmanageactivities) { $url = clone($cmbaseurl); $url->param('delete', $cm->id); $url->param('sr', $cm->sectionnum); $controls['delete'] = [ 'url' => $url, 'icon' => 't/delete', 'name' => get_string('delete'), 'pixattr' => ['class' => ''], 'attr' => [ 'class' => 'editing_delete text-danger', 'data-action' => ($usecomponents) ? 'cmDelete' : 'delete', 'data-sectionreturn' => $sectionreturn, 'data-id' => $cm->id, ], ]; } // Add section page permalink. if ( has_any_capability([ 'moodle/course:movesections', 'moodle/course:update', 'moodle/course:sectionvisibility', ], $coursecontext) ) { $sectionlink = new url( '/course/section.php', ['id' => $section->id] ); $controls['permalink'] = [ 'url' => $sectionlink, 'icon' => 'i/link', 'name' => get_string('sectionlink', 'course'), 'pixattr' => ['class' => ''], 'attr' => [ 'data-action' => 'permalink', ], ]; } return $controls; } } PK$9\VƸ((5format/classes/output/local/content/cm/visibility.phpnu[. /** * Contains the default activity availability information. * * @package core_courseformat * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content\cm; use action_menu_link_secondary; use core\output\local\action_menu\subpanel as action_menu_subpanel; use cm_info; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use core\output\choicelist; use core\output\local\dropdown\status; use core\output\named_templatable; use pix_icon; use renderable; use section_info; use stdClass; /** * Base class to render a course module availability inside a course format. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class visibility implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format */ protected $format; /** @var section_info the section object */ protected $section; /** @var cm_info the course module instance */ protected $mod; /** * Constructor. * @param course_format $format the course format * @param section_info $section the section info * @param cm_info $mod the course module ionfo */ public function __construct(course_format $format, section_info $section, cm_info $mod) { $this->format = $format; $this->section = $section; $this->mod = $mod; } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass|null data context for a mustache template */ public function export_for_template(\renderer_base $output): ?stdClass { if (!$this->show_visibility()) { return null; } if (!$this->format->show_activity_editor_options($this->mod)) { return null; } $format = $this->format; // In rare legacy cases, the section could be stealth (orphaned) but they are not editable. if (!$format->show_editor() || !has_capability('moodle/course:activityvisibility', $this->mod->context)) { return $this->build_static_data($output); } else { return $this->build_editor_data($output); } } /** * Check if the visibility is displayed. * @return bool */ protected function show_visibility(): bool { return !$this->mod->visible || $this->mod->is_stealth(); } /** * Get the icon for the section visibility. * @param string $selected the visibility selected value * @return pix_icon */ protected function get_icon(string $selected): pix_icon { if ($selected === 'hide') { return new pix_icon('t/show', ''); } else if ($selected === 'stealth') { return new pix_icon('t/stealth', ''); } else { return new pix_icon('t/hide', ''); } } /** * Build the data for the editor. * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass|null data context for a mustache template */ public function build_editor_data(\renderer_base $output): ?stdClass { $choice = $this->get_choice_list(); return $this->get_dropdown_data($output, $choice); } /** * Build the data for the interactive dropdown. * @param \renderer_base $output * @param choicelist $choice the choice list * @return stdClass */ protected function get_dropdown_data( \renderer_base $output, choicelist $choice, ): stdClass { $badgetext = $output->visually_hidden_text(get_string('availability')); if (!$this->mod->visible) { $badgetext .= get_string('hiddenfromstudents'); $icon = $this->get_icon('hide'); } else if ($this->mod->is_stealth()) { $badgetext .= get_string('hiddenoncoursepage'); $icon = $this->get_icon('stealth'); } else { $badgetext .= get_string("availability_show", 'core_courseformat'); $icon = $this->get_icon('show'); } $dropdown = new status( $output->render($icon) . ' ' . $badgetext, $choice, ['dialogwidth' => status::WIDTH['big']], ); return (object) [ 'isInteractive' => true, 'dropwdown' => $dropdown->export_for_template($output), ]; } /** * Get the availability choice list. * @return choicelist */ public function get_choice_list(): choicelist { $choice = $this->create_choice_list(); $choice->set_selected_value($this->get_selected_choice_value()); return $choice; } /** * Return the cm availability menu item. * * By default, the cm availability is displayed as a menu item subpanel. * However, it can be simplified when there is only one option and * it is not stealth (stealth require a subpanel to inform the user). * * @return action_menu_link_secondary|action_menu_subpanel|null */ public function get_menu_item(): action_menu_link_secondary|action_menu_subpanel|null { $choice = $this->get_choice_list(); $selectableoptions = $choice->get_selectable_options(); if (count($selectableoptions) === 0) { return null; } // Visible activities in hidden sections are always considered stealth. if ($this->section->visible && count($selectableoptions) === 1) { $option = reset($selectableoptions); $actionlabel = $option->value === 'show' ? 'modshow' : 'modhide'; return new action_menu_link_secondary( $option->url, $option->icon, get_string($actionlabel, 'moodle'), $choice->get_option_extras($option->value) ); } return new action_menu_subpanel( get_string('availability', 'moodle'), $choice, ['class' => 'editing_availability'], new pix_icon('t/hide', '', 'moodle', ['class' => 'iconsmall']) ); } /** * Get the selected choice value depending on the course, section and stealth settings. * @return string */ protected function get_selected_choice_value(): string { if (!$this->mod->visible) { return 'hide'; } if (!$this->mod->is_stealth()) { return 'show'; } if (!$this->section->visible) { // All visible activities in a hidden sections are considered stealth // but they don't use the stealth attribute for it. It is just implicit. return 'show'; } return 'stealth'; } /** * Create a choice list for the dropdown. * @return choicelist the choice list */ protected function create_choice_list(): choicelist { global $CFG; $choice = new choicelist(); if ($this->section->visible || $this->mod->has_view()) { $label = $this->section->visible ? 'show' : 'stealth'; $choice->add_option( 'show', get_string("availability_{$label}", 'core_courseformat'), $this->get_option_data($label, 'cmShow', 'cm_show') ); } $choice->add_option( 'hide', get_string('availability_hide', 'core_courseformat'), $this->get_option_data('hide', 'cmHide', 'cm_hide') ); if ($CFG->allowstealth && $this->format->allow_stealth_module_visibility($this->mod, $this->section)) { $choice->add_option( 'stealth', get_string('availability_stealth', 'core_courseformat'), $this->get_option_data('stealth', 'cmStealth', 'cm_stealth') ); } return $choice; } /** * Get the data for the option. * @param string $name the name of the option * @param string $mutation the mutation name * @param string $stateaction the state action name * @return array */ private function get_option_data(string $name, string $mutation, string $stateaction): array { $format = $this->format; $nonajaxurl = $format->get_update_url( action: $stateaction, ids: [$this->mod->id], returnurl: $format->get_view_url($format->get_sectionnum(), ['navigation' => true]), ); return [ 'description' => get_string("availability_{$name}_help", 'core_courseformat'), 'icon' => $this->get_icon($name), 'url' => $nonajaxurl, 'extras' => [ 'data-id' => $this->mod->id, 'data-action' => $mutation, ] ]; } /** * Build the static badges data. * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass|null data context for a mustache template */ public function build_static_data(\renderer_base $output): ?stdClass { $data = (object) [ 'isInteractive' => false, ]; if (!$this->mod->visible) { $data->modhiddenfromstudents = true; } else if ($this->mod->is_stealth()) { $data->modstealth = true; } return $data; } } PK$9\ʩ{ + + 1format/classes/output/local/content/cm/cmicon.phpnu[. /** * Contains the default activity icon. * * @package core_courseformat * @copyright 2023 Mikel Martin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content\cm; use cm_info; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; use stdClass; /** * Base class to render a course module icon. * * @package core_courseformat * @copyright 2023 Mikel Martin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cmicon implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format */ protected $format; /** @var cm_info the course module instance */ protected $mod; /** * Constructor. * * @param course_format $format the course format * @param cm_info $mod the course module ionfo */ public function __construct( course_format $format, cm_info $mod, ) { $this->format = $format; $this->mod = $mod; } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output): array { $mod = $this->mod; if (!$this->is_icon_visible()) { // Nothing to be displayed to the user. return []; } $iconurl = $mod->get_icon_url(); $iconclass = $iconurl->get_param('filtericon') ? '' : 'nofilter'; $isbranded = component_callback('mod_' . $mod->modname, 'is_branded', [], false); $data = [ 'uservisible' => $mod->uservisible, 'url' => $mod->url, 'icon' => $iconurl, 'iconclass' => $iconclass, 'modname' => $mod->modname, 'pluginname' => get_string('pluginname', 'mod_' . $mod->modname), 'showtooltip' => $this->format->show_editor(), 'purpose' => plugin_supports('mod', $mod->modname, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER), 'branded' => $isbranded, ]; return $data; } /** * Return if the activity has a visible icon. * * @return bool if the icon should be shown. */ public function is_icon_visible(): bool { return $this->mod->is_visible_on_course_page() && $this->mod->url; } } PK$9\KAA6format/classes/output/local/content/cm/controlmenu.phpnu[. /** * Contains the default activity control menu. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content\cm; use cm_info; use core\context\module as context_module; use core\output\action_menu; use core\output\action_menu\link; use core\output\action_menu\link_secondary; use core\output\action_menu\subpanel; use core\output\pix_icon; use core\output\renderer_base; use core_courseformat\base as course_format; use core_courseformat\output\local\content\basecontrolmenu; use core_courseformat\sectiondelegate; use core\url; use section_info; use stdClass; /** * Base class to render a course module menu inside a course format. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class controlmenu extends basecontrolmenu { /** @var array optional display options */ protected $displayoptions; /** @var context_module|null modcontext the module context if any */ protected ?context_module $modcontext = null; /** @var bool $canmanageactivities Optimization to know if the user can manage activities */ protected bool $canmanageactivities; /** @var url $basemodurl the base mod.php url */ protected url $basemodurl; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info * @param cm_info $mod the course module info * @param array $displayoptions optional extra display options */ public function __construct(course_format $format, section_info $section, cm_info $mod, array $displayoptions = []) { parent::__construct($format, $section, $mod, $mod->id); $this->displayoptions = $displayoptions; $this->modcontext = context_module::instance($mod->id); $this->canmanageactivities = has_capability('moodle/course:manageactivities', $this->modcontext); $this->basemodurl = new url('/course/mod.php'); $sectionnumreturn = $format->get_sectionnum(); if ($sectionnumreturn !== null) { $this->basemodurl->param('sr', $sectionnumreturn); } } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output typically, the renderer that's calling this function * @return stdClass|null data context for a mustache template */ public function export_for_template(renderer_base $output): ?stdClass { $mod = $this->mod; if (!$this->format->show_activity_editor_options($this->mod)) { return null; } $menu = $this->get_action_menu($output); if (empty($menu)) { return new stdClass(); } $data = (object)[ 'menu' => $menu->export_for_template($output), 'hasmenu' => true, 'id' => $this->menuid, ]; // After icons. if (!empty($mod->afterediticons)) { $data->afterediticons = $mod->afterediticons; } return $data; } /** * Generate the action menu element. * * This method is public in case some block needs to modify the menu before output it. * @param renderer_base $output typically, the renderer that's calling this function * @return action_menu|null the activity action menu */ public function get_action_menu(renderer_base $output): ?action_menu { if (!empty($this->menu)) { return $this->menu; } // In case module is delegating a section, we should return delegated section action menu. if ($delegated = $this->mod->get_delegated_section_info()) { $controlmenuclass = $this->format->get_output_classname('content\\cm\\delegatedcontrolmenu'); $controlmenu = new $controlmenuclass($this->format, $delegated, $this->mod); return $controlmenu->get_action_menu($output); } // TODO remove this if as part of MDL-83530. if (!$this->format->supports_components()) { $this->menu = $this->get_action_menu_legacy($output); return $this->menu; } $controls = $this->get_cm_control_items(); return $this->format_controls($controls); } /** * Generate the edit control items of a course module. * * This method uses course_get_cm_edit_actions function to get the cm actions. * However, format plugins can override the method to add or remove elements * from the menu. * * @return array of edit control items */ public function get_cm_control_items(): ?array { $controls = []; $controls['update'] = $this->get_cm_edit_item(); $controls['move'] = $this->get_cm_move_item(); $controls['moveright'] = $this->get_cm_moveend_item(); $controls['moveleft'] = $this->get_cm_movestart_item(); $controls['availability'] = $this->get_cm_visibility_item(); $controls['duplicate'] = $this->get_cm_duplicate_item(); $controls['assign'] = $this->get_cm_assign_item(); $controls['groupmode'] = $this->get_cm_groupmode_item(); $controls['delete'] = $this->get_cm_delete_item(); return $controls; } /** * Generates the edit settings item for a course module. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_cm_edit_item(): ?link { if (!$this->canmanageactivities) { return null; } $url = new url($this->basemodurl, ['update' => $this->mod->id]); return new link_secondary( url: $url, icon: new pix_icon('i/settings', ''), text: get_string('editsettings'), attributes: [ 'class' => 'editing_update', ], ); } /** * Generates the move item for a course module. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_cm_move_item(): ?link { // Only show the move link if we are not already in the section view page. if (!$this->canmanageactivities) { return null; } $url = new url($this->basemodurl); return new link_secondary( url: $url, icon: new pix_icon('i/dragdrop', ''), text: get_string('move'), attributes: [ // This tool requires ajax and will appear only when the frontend state is ready. 'class' => 'editing_movecm waitstate', 'data-action' => 'moveCm', 'data-id' => $this->mod->id, ], ); } /** * Check if the course module can be indented. * * @return bool */ protected function can_indent_cm(): bool { return $this->canmanageactivities && !sectiondelegate::has_delegate_class('mod_'.$this->mod->modname) && empty($this->displayoptions['disableindentation']) && $this->format->uses_indentation(); } /** * Generates the move right item for a course module. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_cm_moveend_item(): ?link { if (!$this->can_indent_cm() || $this->mod->indent > 0) { return null; } $url = $this->format->get_update_url( action: 'cm_moveright', ids: [$this->mod->id], returnurl: $this->baseurl, ); $icon = (right_to_left()) ? 't/left' : 't/right'; return new link_secondary( url: $url, icon: new pix_icon($icon, ''), text: get_string('moveright'), attributes: [ 'class' => 'editing_moveright', 'data-action' => 'cmMoveRight', 'data-keepopen' => true, 'data-sectionreturn' => $this->format->get_sectionnum(), 'data-id' => $this->mod->id, ], ); } /** * Generates the move left item for a course module. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_cm_movestart_item(): ?link { if (!$this->can_indent_cm() || $this->mod->indent <= 0) { return null; } $url = $this->format->get_update_url( action: 'cm_moveleft', ids: [$this->mod->id], returnurl: $this->baseurl, ); $icon = (right_to_left()) ? 't/right' : 't/left'; return new link_secondary( url: $url, icon: new pix_icon($icon, ''), text: get_string('moveleft'), attributes: [ 'class' => 'editing_moveleft', 'data-action' => 'cmMoveLeft', 'data-keepopen' => true, 'data-sectionreturn' => $this->format->get_sectionnum(), 'data-id' => $this->mod->id, ], ); } /** * Generates the visibility item for a course module. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_cm_visibility_item(): link_secondary|subpanel|null { if (!has_capability('moodle/course:activityvisibility', $this->modcontext)) { return null; } $outputclass = $this->format->get_output_classname('content\\cm\\visibility'); /** @var \core_courseformat\output\local\content\cm\visibility $output */ $output = new $outputclass($this->format, $this->section, $this->mod); return $output->get_menu_item(); } /** * Generates the duplicate item for a course module. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_cm_duplicate_item(): ?link { if ( !has_all_capabilities( ['moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport'], $this->coursecontext ) || !plugin_supports('mod', $this->mod->modname, FEATURE_BACKUP_MOODLE2) || !course_allowed_module($this->mod->get_course(), $this->mod->modname) ) { return null; } $url = $this->format->get_update_url( action: 'cm_duplicate', ids: [$this->mod->id], returnurl: $this->baseurl, ); return new link_secondary( url: $url, icon: new pix_icon('t/copy', ''), text: get_string('duplicate'), attributes: [ 'class' => 'editing_duplicate', 'data-action' => 'cmDuplicate', 'data-sectionreturn' => $this->format->get_sectionnum(), 'data-id' => $this->mod->id, ], ); } /** * Generates the assign roles item for a course module. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_cm_assign_item(): ?link { if ( !has_capability('moodle/role:assign', $this->modcontext) || sectiondelegate::has_delegate_class('mod_'.$this->mod->modname) ) { return null; } return new link_secondary( url: new url('/admin/roles/assign.php', ['contextid' => $this->modcontext->id]), icon: new pix_icon('t/assignroles', ''), text: get_string('assignroles', 'role'), attributes: [ 'class' => 'editing_assign', 'data-sectionreturn' => $this->format->get_sectionnum(), ], ); } /** * Generates the group mode item for a course module. * * @return subpanel|null The menu item if applicable, otherwise null. */ protected function get_cm_groupmode_item(): ?subpanel { if ( !$this->format->show_groupmode($this->mod) || $this->mod->coursegroupmodeforce ) { return null; } $groupmodeclass = $this->format->get_output_classname('content\\cm\\groupmode'); /** @var \core_courseformat\output\local\content\cm\groupmode $groupmode */ $groupmode = new $groupmodeclass($this->format, $this->section, $this->mod); return new subpanel( text: get_string('groupmode', 'group'), subpanel: $groupmode->get_choice_list(), attributes: ['class' => 'editing_groupmode'], icon: new pix_icon('t/groupv', '', 'moodle', ['class' => 'iconsmall']), ); } /** * Generates the delete item for a course module. * * @return link|null The menu item if applicable, otherwise null. */ protected function get_cm_delete_item(): ?link { if (!$this->canmanageactivities) { return null; } $url = $this->format->get_update_url( action: 'cm_delete', ids: [$this->mod->id], returnurl: $this->baseurl, ); return new link_secondary( url: $url, icon: new pix_icon('t/delete', ''), text: get_string('delete'), attributes: [ 'class' => 'editing_delete text-danger', 'data-action' => 'cmDelete', 'data-sectionreturn' => $this->format->get_sectionnum(), 'data-id' => $this->mod->id, ], ); } /** * Generate the action menu element for old course formats. * * This method is public in case some block needs to modify the menu before output it. * * @todo Remove this method in Moodle 6.0 (MDL-83530). * @param \renderer_base $output typically, the renderer that's calling this function * @return action_menu|null the activity action menu */ private function get_action_menu_legacy(\renderer_base $output): ?action_menu { $mod = $this->mod; $controls = $this->cm_control_items(); if (empty($controls)) { return null; } // Convert control array into an action_menu. $menu = new action_menu(); $menu->set_kebab_trigger(get_string('edit')); $menu->attributes['class'] .= ' section-cm-edit-actions commands'; // Prioritise the menu ahead of all other actions. $menu->prioritise = true; $ownerselector = $this->displayoptions['ownerselector'] ?? '#module-' . $mod->id; $menu->set_owner_selector($ownerselector); foreach ($controls as $control) { if ($control instanceof link) { $control->add_class('cm-edit-action'); } $menu->add($control); } $this->menu = $menu; return $menu; } /** * Generate the edit control items of a course module. * * This method uses course_get_cm_edit_actions function to get the cm actions. * However, format plugins can override the method to add or remove elements * from the menu. * * @deprecated since Moodle 5.0 * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return array of edit control items */ #[\core\attribute\deprecated( replacement: 'get_cm_control_items', since: '5.0', mdl: 'MDL-83527', )] protected function cm_control_items() { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); $format = $this->format; $mod = $this->mod; $sectionreturn = $format->get_sectionnum(); if (!empty($this->displayoptions['disableindentation']) || !$format->uses_indentation()) { $indent = -1; } else { $indent = $mod->indent; } return course_get_cm_edit_actions($mod, $indent, $sectionreturn); } } PK$9\b+8format/classes/output/local/content/delegatedsection.phpnu[. /** * Contains the default delegated section course format output class. * * @package core_courseformat * @copyright 2024 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content; /** * Base class to render a delegated section. * * @package core_courseformat * @copyright 2024 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delegatedsection extends section { } PK$9\.!!7format/classes/output/local/content/basecontrolmenu.phpnu[. namespace core_courseformat\output\local\content; use core\context\course as context_course; use core\output\action_menu; use core\output\action_menu\link_secondary; use core\output\named_templatable; use core\output\pix_icon; use core\output\renderable; use core\output\renderer_base; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use core\url; use section_info; use cm_info; use stdClass; /** * Base class to render course element controls. * * @package core_courseformat * @copyright 2024 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class basecontrolmenu implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format class */ protected $format; /** @var section_info the course section class */ protected $section; /** @var cm_info the course module class */ protected $mod; /** @var stdClass the course instance */ protected stdClass $course; /** @var context_course the course context */ protected $coursecontext; /** @var string the menu ID */ protected $menuid; /** @var action_menu the action menu */ protected $menu; /** @var url The base URL for the course or the section */ protected url $baseurl; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info * @param cm_info|null $mod the module info * @param string $menuid the ID value for the menu */ public function __construct(course_format $format, section_info $section, ?cm_info $mod = null, string $menuid = '') { $this->format = $format; $this->section = $section; $this->mod = $mod; $this->menuid = $menuid; $this->course = $format->get_course(); $this->coursecontext = $format->get_context(); $this->baseurl = $format->get_view_url($format->get_sectionnum(), ['navigation' => true]); } /** * Change the default base URL to return after each action. * * @param url $baseurl */ public function set_baseurl(url $baseurl) { $this->baseurl = $baseurl; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output typically, the renderer that's calling this function * @return null|array data context for a mustache template */ public function export_for_template(renderer_base $output): ?stdClass { $menu = $this->get_action_menu($output); if (empty($menu)) { return new stdClass(); } $data = (object)[ 'menu' => $output->render($menu), 'hasmenu' => true, 'id' => $this->menuid, ]; return $data; } /** * Generate the action menu element. * * @param renderer_base $output typically, the renderer that's calling this function * @return action_menu|null the action menu or null if no action menu is available */ public function get_action_menu(renderer_base $output): ?action_menu { if (!empty($this->menu)) { return $this->menu; } $this->menu = $this->get_default_action_menu($output); return $this->menu; } /** * Generate the default action menu. * * This method is public in case some block needs to modify the menu before output it. * * @param renderer_base $output typically, the renderer that's calling this function * @return action_menu|null the action menu */ public function get_default_action_menu(renderer_base $output): ?action_menu { return null; } /** * Format control array into an action_menu. * * @param \renderer_base $output typically, the renderer that's calling this function * @return action_menu|null the action menu */ protected function format_controls(array $controls): ?action_menu { if (empty($controls)) { return null; } $menu = new action_menu(); $menu->set_kebab_trigger(get_string('edit')); $menu->attributes['class'] .= ' section-actions'; $menu->attributes['data-sectionid'] = $this->section->id; foreach ($controls as $item) { // Actions not available for the user can be null. if ($item === null) { continue; } // TODO remove this if as part of MDL-83530. if (is_array($item)) { // Some third party formats from 4.5 and older can use array to define the action menu items. $item = $this->normalize_action_menu_link($item); } $menu->add($item); } return $menu; } /** * Nromalize the action menu item, or return null if it is not possible. * * Traditionally, this class uses array to define the action menu items, * for backward compatibility, this method will normalize the array into * the correct action_menu_link object. * * @todo Remove this method in Moodle 6.0 (MDL-83530). * @param array|null $itemdata the item data * @return void */ private function normalize_action_menu_link( array|null $itemdata ): ?link_secondary { debugging( "Using arrays as action menu items is deprecated, use a compatible menu item instead.", DEBUG_DEVELOPER ); if (empty($itemdata)) { return null; } $url = empty($itemdata['url']) ? '' : $itemdata['url']; $icon = empty($itemdata['icon']) ? '' : $itemdata['icon']; $name = empty($itemdata['name']) ? '' : $itemdata['name']; $attr = empty($itemdata['attr']) ? [] : $itemdata['attr']; $class = empty($itemdata['pixattr']['class']) ? '' : $itemdata['pixattr']['class']; return new link_secondary( url: new url($url), icon: new pix_icon($icon, '', null, ['class' => "smallicon " . $class]), text: $name, attributes: $attr, ); } /** * Generate the edit control items of a section. * * This method must remain public until the final deprecation of section_edit_control_items. * * @return array of edit control items */ public function section_control_items() { return []; } /** * Adds a new control item after a given control item. * * If the control item is not found, the new control item is added at the beginning. * * @param array $controls array of edit control items * @param string $aftername name of the control item after which the new control item will be added * @param string $newkey key of the new control item * @param mixed $newcontrol new control item to be added (anything compatible with an action menu or null) */ protected function add_control_after(array $controls, string $aftername, string $newkey, mixed $newcontrol): array { if (!array_key_exists($aftername, $controls)) { return array_merge([$newkey => $newcontrol], $controls); } $newcontrols = []; $found = false; foreach ($controls as $keyname => $control) { $newcontrols[$keyname] = $control; if ($keyname === $aftername) { $newcontrols[$newkey] = $newcontrol; $found = true; } } if (!$found) { $newcontrols[$newkey] = $newcontrol; } return $newcontrols; } } PK$9\l+  7format/classes/output/local/content/bulkedittoggler.phpnu[. namespace core_courseformat\output\local\content; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; /** * Course bulk edit mode toggler button. * * @package core_courseformat * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class bulkedittoggler implements named_templatable, renderable { use courseformat_named_templatable; /** @var core_courseformat\base the course format class */ protected $format; /** * Constructor. * * @param course_format $format the course format */ public function __construct(course_format $format) { $this->format = $format; } /** * Export this data so it can be used as the context for a mustache template (core/inplace_editable). * * @param renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output) { $section = optional_param('section', 0, PARAM_INT); $format = $this->format; $course = $format->get_course(); $data = (object)[ 'id' => $course->id, 'coursename' => format_string($course->fullname), ]; if ($section) { $data->sectionname = $format->get_generic_section_name(); $data->sectiontitle = $format->get_section_name($section); } return $data; } } PK$9\.9format/classes/output/local/content/sectionnavigation.phpnu[. /** * Contains the default section navigation output class. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content; use context_course; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; use stdClass; /** * Base class to render a course add section navigation. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sectionnavigation implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format class */ protected $format; /** @var int the course displayed section */ protected $sectionno; /** @var stdClass the calculated data to prevent calculations when rendered several times */ protected $data = null; /** * Constructor. * * @param course_format $format the course format * @param int $sectionno the section number */ public function __construct(course_format $format, int $sectionno) { $this->format = $format; $this->sectionno = $sectionno; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { global $USER; if ($this->data !== null) { return $this->data; } $format = $this->format; $course = $format->get_course(); $context = context_course::instance($course->id); $modinfo = $this->format->get_modinfo(); $sections = $modinfo->get_section_info_all(); // FIXME: This is really evil and should by using the navigation API. $canviewhidden = has_capability('moodle/course:viewhiddensections', $context, $USER); $data = (object)[ 'previousurl' => '', 'nexturl' => '', 'larrow' => $output->larrow(), 'rarrow' => $output->rarrow(), 'currentsection' => $this->sectionno, ]; $back = $this->sectionno - 1; while ($back >= 0 && empty($data->previousurl)) { if ($canviewhidden || $sections[$back]->uservisible) { if (!$sections[$back]->visible) { $data->previoushidden = true; } $data->previousname = get_section_name($course, $sections[$back]); $data->previousurl = course_get_url($course, $back, ['navigation' => true]); // If there is no url for the section the link should not be displayed. $data->hasprevious = !empty($data->previousurl); } $back--; } $forward = $this->sectionno + 1; $numsections = course_get_format($course)->get_last_section_number(); while ($forward <= $numsections and empty($data->nexturl)) { if ($canviewhidden || $sections[$forward]->uservisible) { if (!$sections[$forward]->visible) { $data->nexthidden = true; } $data->nextname = get_section_name($course, $sections[$forward]); $data->nexturl = course_get_url($course, $forward, ['navigation' => true]); // If there is no url for the section the link should not be displayed. $data->hasnext = !empty($data->nexturl); } $forward++; } $this->data = $data; return $data; } } PK$9\Z 8format/classes/output/local/content/frontpagesection.phpnu[. /** * Contains the default frontpage section displayer. * * The frontpage has a different wat of rendering the main topic. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content; use context_course; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use moodle_url; use renderable; use section_info; use stdClass; /** * Represents the frontpage section 1. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class frontpagesection implements named_templatable, renderable { use courseformat_named_templatable; /** @var course_format the course format class */ protected $format; /** @var section_info the course section class */ protected $section; /** @var string the section output class name */ protected $sectionclass; /** * Constructor. * * @param course_format $format the course format * @param section_info $section the section info */ public function __construct(course_format $format, section_info $section) { $this->format = $format; $this->section = $section; // Get the necessary classes. $this->sectionclass = $format->get_output_classname('content\\section'); } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { global $USER; $format = $this->format; $section = $this->section; $sectionoutput = new $this->sectionclass($format, $section); $sectionoutput->hide_controls(); if (trim($section->name ?? '') == '') { $sectionoutput->hide_title(); } $data = (object)[ 'editing' => $format->show_editor(), 'sections' => [$sectionoutput->export_for_template($output)], ]; if ($format->show_editor(['moodle/course:update'])) { $data->showsettings = true; $data->settingsurl = new moodle_url('/course/editsection.php', ['id' => $section->id]); } return $data; } } PK$9\gF7format/classes/output/local/content/sectionselector.phpnu[. /** * Contains the default section selector. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_courseformat\output\local\content; use core\output\named_templatable; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; use renderable; use section_info; use stdClass; use url_select; /** * Represents the section selector. * * @package core_courseformat * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sectionselector implements named_templatable, renderable { use courseformat_named_templatable; /** @var string the indenter */ private const INDENTER = '    '; /** @var course_format the course format class */ protected $format; /** @var sectionnavigation the main section navigation class */ protected $navigation; /** @var array $sectionmenu the sections indexed by url. */ protected $sectionmenu = []; /** * Constructor. * * In the current imeplementaiton the seciton selector is almost a variation of the section navigator * but in the 4.0 this selector will be a kind of dropdown menu. When this happens the construct params * will change. * * @param course_format $format the course format * @param sectionnavigation $navigation the current section navigation */ public function __construct(course_format $format, sectionnavigation $navigation) { $this->format = $format; $this->navigation = $navigation; } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { $format = $this->format; $course = $format->get_course(); $modinfo = $this->format->get_modinfo(); $data = $this->navigation->export_for_template($output); $this->sectionmenu[course_get_url($course)->out(false)] = get_string('maincoursepage'); // Add the section selector. $allsections = $modinfo->get_section_info_all(); $disabledlink = $this->get_section_url($course, $allsections[$data->currentsection]); $sectionwithchildren = []; // First get any section with chidren (easier to process later in a regular loop). foreach ($allsections as $section) { if (!$section->uservisible) { unset($allsections[$section->sectionnum]); continue; } $sectiondelegated = $section->get_component_instance(); if ($sectiondelegated) { unset($allsections[$section->sectionnum]); $parentsection = $sectiondelegated->get_parent_section(); // If the section is delegated we need to get the parent section and add the section to the parent section array. if ($parentsection) { $sectionwithchildren[$parentsection->sectionnum][] = $section; } } } foreach ($allsections as $section) { $this->add_section_menu($format, $course, $section); if (isset($sectionwithchildren[$section->sectionnum])) { foreach ($sectionwithchildren[$section->sectionnum] as $subsection) { $this->add_section_menu($format, $course, $subsection, true); } } } $select = new url_select( urls: $this->sectionmenu, selected: '', nothing: ['' => get_string('jumpto')], ); // Disable the current section. if (!is_null($disabledlink)) { $select->set_option_disabled($disabledlink); } $select->class = 'jumpmenu'; $select->formid = 'sectionmenu'; $data->selector = $output->render($select); return $data; } /** * Add a section to the section menu. * * @param course_format $format * @param stdClass $course * @param section_info $section * @param bool $indent */ private function add_section_menu( course_format $format, stdClass $course, section_info $section, bool $indent = false ) { $url = $this->get_section_url($course, $section); if (!is_null($url)) { $indentation = $indent ? self::INDENTER : ''; $this->sectionmenu[$url] = $indentation . $format->get_section_name($section); } } /** * Get the section url. * @param stdClass $course * @param section_info $section * @return string|null */ private function get_section_url(stdClass $course, section_info $section): ?string { return course_get_url($course, (object) $section, ['navigation' => true])?->out(false); } } PK$9\ ܽ>format/classes/output/local/courseformat_named_templatable.phpnu[. namespace core_courseformat\output\local; /** * Base templatable class for coursformat templateables which are typically overridden by course formats. * * @package core_courseformat * @copyright 2022 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait courseformat_named_templatable { /** * Get the name of the template to use for this templatable. * * @param \renderer_base $renderer The renderer requesting the template name * @return string */ public function get_template_name(\renderer_base $renderer): string { $fullpath = str_replace('\\', '/', get_class($this)); $specialrenderers = '@^.*/output/(local|courseformat)/(?