ÿØÿà 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-Ї Ç db/upgrade.php000064400000004545152266413450007311 0ustar00. /** * 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; } lang/en/format_topics.php000064400000003656152266413450011473 0ustar00. /** * 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'; backup/moodle2/restore_format_topics_plugin.class.php000064400000011412152266413450017170 0ustar00. /** * 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]); } } } } } lib.php000064400000041545152266413450006044 0ustar00. /** * 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); } } version.php000064400000002240152266413450006750 0ustar00. /** * 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). classes/privacy/provider.php000064400000003032152266413450012227 0ustar00. /** * 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'; } }classes/output/renderer.php000064400000005041152266413450012070 0ustar00. 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)); } } classes/output/courseformat/content/section/controlmenu.php000064400000017113152266413450020461 0ustar00. /** * 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; } } classes/output/courseformat/content/section.php000064400000003650152266413450016115 0ustar00. /** * 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; } } classes/output/courseformat/content.php000064400000003652152266413450014453 0ustar00. /** * 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); } } classes/courseformat/stateactions.php000064400000010020152266413450014125 0ustar00. 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); } } } } amd/src/mutations.js000064400000007406152266413450010474 0ustar00// 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', }); }; amd/src/section.js000064400000007677152266413450010127 0ustar00// 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, }); } }; amd/build/mutations.min.js.map000064400000011454152266413450012340 0ustar00{"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"}amd/build/section.min.js.map000064400000013075152266413450011762 0ustar00{"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"}amd/build/mutations.min.js000064400000003630152266413450011561 0ustar00define("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.mapamd/build/section.min.js000064400000004725152266413450011210 0ustar00define("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.mapUPGRADING.md000064400000001073152266413450006417 0ustar00# 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) tests/format_topics_test.php000064400000026526152266413450012352 0ustar00. 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()); } } tests/behat/edit_delete_sections.feature000064400000013766152266413450014551 0ustar00@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" tests/behat/highlight_sections.feature000064400000005741152266413450014243 0ustar00@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 tests/courseformat/stateactions_test.php000064400000017057152266413450014712 0ustar00. 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, ], ]; } } format.php000064400000004116152266413450006557 0ustar00. /** * 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); settings.php000064400000002520152266413450007124 0ustar00. /** * 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 )); }