ÿØÿà 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|\E %be e 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|\ Yˮ 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|\i  <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|\u?eCeCtopics/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|\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|\TǑ#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|\^1! ! "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|\֜/KKBtopics/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|\x.6topics/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|\.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|\a M,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|\y@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|\*%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|\3v,,%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|\ {i==#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|\*!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|\#> 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|\d;;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|\L)V-V-#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|\c/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|\_pf -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|\q-///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|\C&NNtopics/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|\~PPtopics/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|\(o&&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|\Vw1^9^9formatlegacy.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|\VoAAsingleactivity/styles.cssnu[.format-singleactivity .tree_item.orphaned a { color: red; } PK|\nn0singleactivity/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|\/ϓZZsingleactivity/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|\1Wsingleactivity/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|\wȒ**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|\3֗j+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|\ *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|\&h! ! 3singleactivity/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|\f[[6singleactivity/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|\a LL0singleactivity/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|\ujj5singleactivity/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|\J~csingleactivity/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|\xZ  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|\ 5Re "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|\kvclasses/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|\`d55"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|\ϋ"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|\sclasses/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|\әZ Z 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|\ouclasses/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|\0M M 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|\VKclasses/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|\}@@'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|\6h7  *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|\+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|\xo`!`!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|\JJ 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|\h22classes/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|\??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|\g%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|\H-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|\v2!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|\ϋ 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|\!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|\sC|..&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|\rN %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|\E?,.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|\|G .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|\M-d7classes/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|\m0&&/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|\ҝ.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|\Oj>>#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|\F&+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|\!&&5classes/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|\/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|\fFDC C 0classes/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|\ߺ/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|\ p"2classes/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|\K~3classes/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|\7؃/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|\gaa4classes/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|\)4646(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|\Ooo0classes/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|\).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|\xᦙ-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|\x͜*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|\`vTT)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|\mCC8classes/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|\VƸ((.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|\ʩ{ + + *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|\KAA/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|\b+1classes/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|\.!!0classes/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|\l+  0classes/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|\.2classes/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|\Z 1classes/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|\gF0classes/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|\ ܽ7classes/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)/(?