├── styles_bootstrapbase.css ├── styles.css ├── version.php ├── README.md ├── format.php ├── db ├── events.php ├── upgrade.php └── upgradelib.php ├── format.js ├── classes └── observer.php ├── lang └── en │ └── format_periods.php ├── tests ├── observer_test.php ├── format_periods_upgrade_test.php └── format_periods_test.php ├── backup └── moodle2 │ └── restore_format_periods_plugin.class.php ├── renderer.php ├── periodduration.php └── lib.php /styles_bootstrapbase.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | .course-content ul.periods li.section .left {padding-top:22px;} 3 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | .course-content ul.periods { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | .course-content ul.periods li.section {list-style: none;margin:0 0 5px 0;padding:0;} 7 | /*.course-content ul.periods li.section .content {margin:0 20px;}*/ 8 | .course-content ul.periods li.section .left, 9 | .course-content ul.periods li.section .right {width:40px;padding: 0 6px;} 10 | .course-content ul.periods li.section .right img.icon { padding: 0 0 4px 0;} 11 | .course-content ul.periods div.sectiondates {font-style:italic; color: #888;} 12 | .jsenabled .course-content ul.periods li.section .left, 13 | .jsenabled .course-content ul.periods li.section .right {width:auto;} 14 | .course-content ul.periods li.section .left .section-handle img.icon { padding:0; vertical-align: baseline; } 15 | .course-content ul.periods li.section .section_action_menu .textmenu, 16 | .course-content ul.periods li.section .section_action_menu .menu-action-text { white-space: nowrap; } 17 | 18 | .course-content ul.periods li.section .summary { 19 | margin-left: 25px; 20 | } -------------------------------------------------------------------------------- /version.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Version details 19 | * 20 | * @package format_periods 21 | * @copyright 2014 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | $plugin->version = 2017050501; // The current plugin version (Date: YYYYMMDDXX). 28 | $plugin->requires = 2017050300; // Requires this Moodle version. 29 | $plugin->component = 'format_periods'; // Full name of the plugin (used for diagnostics). 30 | $plugin->release = "3.3.0"; 31 | $plugin->maturity = MATURITY_STABLE; 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | moodle-format_periods 2 | ===================== 3 | 4 | Version 3.3.0 5 | ------------- 6 | 7 | - Compatibility with Moodle 3.3 8 | - Stealth activities 9 | - Removed 'Number of sections', sections can be added when needed 10 | - Automatic end date calculation 11 | 12 | Version 3.0.4 13 | ------------- 14 | 15 | - Compatibility with Moodle 3.2, Boost and PHP7.1 (still works on previous versions) 16 | 17 | Version 3.0.3 18 | ------------- 19 | 20 | - Compatibility with section name editing in Moodle 3.1 (still works on previous 21 | versions) 22 | 23 | Version 3.0.2 24 | ------------- 25 | 26 | - Allow to delete periods 27 | 28 | Version 3.0.1 29 | ------------- 30 | 31 | - Fixed localisation of period duration, see issues #1 and #2 in github 32 | 33 | Version 3.0.0 34 | ------------- 35 | 36 | - Compatibility with Moodle 3.0 37 | 38 | Version 2.8.2 39 | ------------- 40 | 41 | - On the course page display for teachers the period dates if the section name 42 | was overridden and also duration if it is not standard. 43 | - Allow to choose format for the dates 44 | - Fixed as many codechecker/moodlecheck complains as possible 45 | 46 | Version 2.8.1 (Initial version) 47 | ------------------------------- 48 | 49 | This course format allows to set duration for each section (period) in days, 50 | weeks, months or years. Each individual section (period) may override this 51 | duration. 52 | 53 | The course settings allow automatically collapse or hide past or future periods. 54 | -------------------------------------------------------------------------------- /format.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Periods course format. Display the whole course as "periods" made of modules. 19 | * 20 | * @package format_periods 21 | * @copyright 2014 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | require_once($CFG->libdir.'/filelib.php'); 28 | require_once($CFG->libdir.'/completionlib.php'); 29 | 30 | // Make sure all sections are created. 31 | $course = course_get_format($course)->get_course(); 32 | 33 | $renderer = $PAGE->get_renderer('format_periods'); 34 | 35 | if (!empty($displaysection)) { 36 | $renderer->print_single_section_page($course, null, null, null, null, $displaysection); 37 | } else { 38 | $renderer->print_multiple_section_page($course, null, null, null, null); 39 | } 40 | 41 | $PAGE->requires->js('/course/format/periods/format.js'); 42 | -------------------------------------------------------------------------------- /db/events.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Format periods event handler definition. 19 | * 20 | * @package format_periods 21 | * @copyright 2017 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | $observers = array( 28 | array( 29 | 'eventname' => '\core\event\course_updated', 30 | 'callback' => 'format_periods_observer::course_updated', 31 | ), 32 | array( 33 | 'eventname' => '\core\event\course_section_created', 34 | 'callback' => 'format_periods_observer::course_section_created', 35 | ), 36 | array( 37 | 'eventname' => '\core\event\course_section_updated', 38 | 'callback' => 'format_periods_observer::course_section_updated', 39 | ), 40 | array( 41 | 'eventname' => '\core\event\course_section_deleted', 42 | 'callback' => 'format_periods_observer::course_section_deleted', 43 | ) 44 | ); 45 | -------------------------------------------------------------------------------- /db/upgrade.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Upgrade scripts for course format "periods" 19 | * 20 | * @package format_periods 21 | * @copyright 2017 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | /** 28 | * Upgrade script for format_periods 29 | * 30 | * @param int $oldversion the version we are upgrading from 31 | * @return bool result 32 | */ 33 | function xmldb_format_periods_upgrade($oldversion) { 34 | global $CFG, $DB; 35 | 36 | require_once($CFG->dirroot . '/course/format/periods/db/upgradelib.php'); 37 | 38 | if ($oldversion < 2017050500) { 39 | 40 | // Remove 'numsections' option and hide or delete orphaned sections. 41 | format_periods_upgrade_remove_numsections(); 42 | 43 | upgrade_plugin_savepoint(true, 2017050500, 'format', 'periods'); 44 | } 45 | 46 | if ($oldversion < 2017050501) { 47 | 48 | // Set 'automaticenddate' for existing courses. 49 | format_periods_upgrade_automaticenddate(); 50 | 51 | upgrade_plugin_savepoint(true, 2017050501, 'format', 'periods'); 52 | } 53 | 54 | return true; 55 | } 56 | -------------------------------------------------------------------------------- /format.js: -------------------------------------------------------------------------------- 1 | // Javascript functions for Periods course format 2 | 3 | M.course = M.course || {}; 4 | 5 | M.course.format = M.course.format || {}; 6 | 7 | /** 8 | * Get sections config for this format 9 | * 10 | * The section structure is: 11 | * 16 | * 17 | * @return {object} section list configuration 18 | */ 19 | M.course.format.get_config = function() { 20 | return { 21 | container_node : 'ul', 22 | container_class : 'periods', 23 | section_node : 'li', 24 | section_class : 'section' 25 | }; 26 | } 27 | 28 | /** 29 | * Swap section 30 | * 31 | * @param {YUI} Y YUI3 instance 32 | * @param {string} node1 node to swap to 33 | * @param {string} node2 node to swap with 34 | * @return {NodeList} section list 35 | */ 36 | M.course.format.swap_sections = function(Y, node1, node2) { 37 | var CSS = { 38 | COURSECONTENT : 'course-content', 39 | SECTIONADDMENUS : 'section_add_menus' 40 | }; 41 | 42 | var sectionlist = Y.Node.all('.' + CSS.COURSECONTENT + ' ' + M.course.format.get_section_selector(Y)); 43 | // Swap menus. 44 | sectionlist.item(node1).one('.' + CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.' + CSS.SECTIONADDMENUS)); 45 | } 46 | 47 | /** 48 | * Process sections after ajax response 49 | * 50 | * @param {YUI} Y YUI3 instance 51 | * @param {array} response ajax response 52 | * @param {string} sectionfrom first affected section 53 | * @param {string} sectionto last affected section 54 | * @return void 55 | */ 56 | M.course.format.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) { 57 | var CSS = { 58 | SECTIONNAME : 'sectionname' 59 | }, 60 | SELECTORS = { 61 | SECTIONLEFTSIDE : '.left .section-handle .icon' 62 | }; 63 | 64 | if (response.action == 'move') { 65 | // If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates. 66 | if (sectionfrom > sectionto) { 67 | var temp = sectionto; 68 | sectionto = sectionfrom; 69 | sectionfrom = temp; 70 | } 71 | 72 | // Update titles and move icons in all affected sections. 73 | var ele, str, stridx, newstr; 74 | 75 | for (var i = sectionfrom; i <= sectionto; i++) { 76 | // Update section title. 77 | var content = Y.Node.create('' + response.sectiontitles[i] + ''); 78 | sectionlist.item(i).all('.'+CSS.SECTIONNAME).setHTML(content); 79 | 80 | // Update move icon. 81 | ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE); 82 | str = ele.getAttribute('alt'); 83 | stridx = str.lastIndexOf(' '); 84 | newstr = str.substr(0, stridx + 1) + i; 85 | ele.setAttribute('alt', newstr); 86 | ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed. 87 | 88 | // Remove the current class as section has been moved. 89 | sectionlist.item(i).removeClass('current'); 90 | } 91 | // If there is a current section, apply corresponding class in order to highlight it. 92 | if (response.current !== -1) { 93 | // Add current class to the required section. 94 | sectionlist.item(response.current).addClass('current'); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /classes/observer.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Event observers used by the periods course format. 19 | * 20 | * @package format_periods 21 | * @copyright 2017 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | /** 28 | * Event observer for format_periods. 29 | * 30 | * @package format_periods 31 | * @copyright 2017 Marina Glancy 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class format_periods_observer { 35 | 36 | /** 37 | * Triggered via \core\event\course_updated event. 38 | * 39 | * @param \core\event\course_updated $event 40 | */ 41 | public static function course_updated(\core\event\course_updated $event) { 42 | if (class_exists('format_periods', false)) { 43 | // If class format_periods was never loaded, this is definitely not a course in 'periods' format. 44 | $course = $event->get_record_snapshot('course', $event->courseid); 45 | format_periods::update_end_date($event->courseid); 46 | } 47 | } 48 | 49 | /** 50 | * Triggered via \core\event\course_section_created event. 51 | * 52 | * @param \core\event\course_section_created $event 53 | */ 54 | public static function course_section_created(\core\event\course_section_created $event) { 55 | if (class_exists('format_periods', false)) { 56 | // If class format_periods was never loaded, this is definitely not a course in 'periods' format. 57 | // Course may still be in another format but format_periods::update_end_date() will check it. 58 | format_periods::update_end_date($event->courseid); 59 | } 60 | } 61 | 62 | /** 63 | * Triggered via \core\event\course_section_updated event. 64 | * 65 | * @param \core\event\course_section_updated $event 66 | */ 67 | public static function course_section_updated(\core\event\course_section_updated $event) { 68 | if (class_exists('format_periods', false)) { 69 | // If class format_periods was never loaded, this is definitely not a course in 'periods' format. 70 | // Course may still be in another format but format_periods::update_end_date() will check it. 71 | format_periods::update_end_date($event->courseid); 72 | } 73 | } 74 | 75 | /** 76 | * Triggered via \core\event\course_section_deleted event. 77 | * 78 | * @param \core\event\course_section_deleted $event 79 | */ 80 | public static function course_section_deleted(\core\event\course_section_deleted $event) { 81 | if (class_exists('format_periods', false)) { 82 | // If class format_periods was never loaded, this is definitely not a course in 'periods' format. 83 | // Course may still be in another format but format_periods::update_end_date() will check it. 84 | format_periods::update_end_date($event->courseid); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lang/en/format_periods.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Strings for component 'format_periods', language 'en', branch 'MOODLE_20_STABLE' 19 | * 20 | * @package format_periods 21 | * @copyright 2014 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | $string['automaticenddate'] = 'Calculate the end date from the end of the last period'; 26 | $string['automaticenddate_help'] = 'If enabled, the end date for the course will be automatically calculated from the end date of the last period.'; 27 | $string['currentsection'] = 'This period'; 28 | $string['customdatesformat'] = 'Custom'; 29 | $string['datesformat'] = 'Dates format'; 30 | $string['datesformat_help'] = 'Select the format of the dates that is displayed in the default period name'; 31 | $string['datesformatcustom'] = 'Custom dates format'; 32 | $string['datesformatcustom_help'] = 'Specify custom date format for the dates. See php manual for syntax'; 33 | $string['deletesection'] = 'Delete period'; 34 | $string['editsection'] = 'Edit period'; 35 | $string['editsectionname'] = 'Edit period name'; 36 | $string['futuresneakpeek'] = 'Future sneak peek'; 37 | $string['futuresneakpeek_help'] = 'Treat periods that start earlier than this interval as current (for example this could allow students to see the next week two days before the end of the current week)'; 38 | $string['hidecompletely'] = 'Hide completely'; 39 | $string['hidefromcourseview'] = 'Hide from the course page'; 40 | $string['hidefromothers'] = 'Hide period'; 41 | $string['newsectionname'] = 'New name for period {$a}'; 42 | $string['notavailable'] = 'Not available yet'; 43 | $string['numberperiods'] = 'Number of periods'; 44 | $string['page-course-view-periods'] = 'Any course main page in periods format'; 45 | $string['page-course-view-periods-x'] = 'Any course page in periods format'; 46 | $string['perioddurationdefault'] = 'Default period duration'; 47 | $string['perioddurationoverride'] = 'Override period duration'; 48 | $string['perioddurationdefault_help'] = 'Set the duration of one period. It can be overridden for individual periods'; 49 | $string['perioddurationoverride_help'] = 'Set the duration of this period. If not set the default value for the course will be used'; 50 | $string['pluginname'] = 'Periods format'; 51 | $string['sameaspast'] = 'Same as past periods'; 52 | $string['sameascurrent'] = 'Same as current periods'; 53 | $string['section0name'] = 'General'; 54 | $string['sectiondates'] = 'Period dates: {$a->dates}'; 55 | $string['sectiondatesduration'] = 'Period dates: {$a->dates}; period duration: {$a->duration}'; 56 | $string['sectionduration'] = 'Period duration: {$a->duration}'; 57 | $string['sectionname'] = 'Period'; 58 | $string['showcollapsed'] = 'Show each period as a link to its own page'; 59 | $string['showexpanded'] = 'Show all periods on one page'; 60 | $string['showfromothers'] = 'Show period'; 61 | $string['showfutureperiods'] = 'Show future periods'; 62 | $string['showfutureperiods_help'] = 'Allows to automatically display future periods as links, as not available or hide them completely'; 63 | $string['shownotavailable'] = 'Show as not available'; 64 | $string['showpastcompleted'] = 'Show past completed periods'; 65 | $string['showpastcompleted_help'] = 'Defines how to show periods in the past where all activities have been completed. Completion must be enabled for all modules in the section.'; 66 | $string['showpastperiods'] = 'Show past periods'; 67 | $string['showpastperiods_help'] = 'Defines whether to show or to hide the periods that have the end date in the past. "Hide from the course page" means that the activities will not be shown on the course page but they will be visible in the gradebook and other reports.'; 68 | $string['showperiods'] = 'Show current periods'; 69 | $string['showperiods_help'] = 'Defines how to display periods by default. This can be overridden for past or future periods below'; 70 | -------------------------------------------------------------------------------- /db/upgradelib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Upgrade scripts for course format "periods" 19 | * 20 | * @package format_periods 21 | * @copyright 2017 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | /** 28 | * This method finds all courses in 'periods' format that have actual number of sections 29 | * bigger than their 'numsections' course format option. 30 | * For each such course we call {@link format_periods_upgrade_hide_extra_sections()} and 31 | * either delete or hide "orphaned" sections. 32 | */ 33 | function format_periods_upgrade_remove_numsections() { 34 | global $DB; 35 | 36 | $sql1 = "SELECT c.id, max(cs.section) AS sectionsactual 37 | FROM {course} c 38 | JOIN {course_sections} cs ON cs.course = c.id 39 | WHERE c.format = :format1 40 | GROUP BY c.id"; 41 | 42 | $sql2 = "SELECT c.id, n.value AS numsections 43 | FROM {course} c 44 | JOIN {course_format_options} n ON n.courseid = c.id AND n.format = :format1 AND n.name = :numsections AND n.sectionid = 0 45 | WHERE c.format = :format2"; 46 | 47 | $params = ['format1' => 'periods', 'format2' => 'periods', 'numsections' => 'numsections']; 48 | 49 | $actual = $DB->get_records_sql_menu($sql1, $params); 50 | $numsections = $DB->get_records_sql_menu($sql2, $params); 51 | $needfixing = []; 52 | 53 | $defaultnumsections = get_config('moodlecourse', 'numsections'); 54 | 55 | foreach ($actual as $courseid => $sectionsactual) { 56 | if (array_key_exists($courseid, $numsections)) { 57 | $n = (int)$numsections[$courseid]; 58 | } else { 59 | $n = $defaultnumsections; 60 | } 61 | if ($sectionsactual > $n) { 62 | $needfixing[$courseid] = $n; 63 | } 64 | } 65 | unset($actual); 66 | unset($numsections); 67 | 68 | foreach ($needfixing as $courseid => $numsections) { 69 | format_periods_upgrade_hide_extra_sections($courseid, $numsections); 70 | } 71 | 72 | $DB->delete_records('course_format_options', ['format' => 'periods', 'sectionid' => 0, 'name' => 'numsections']); 73 | } 74 | 75 | /** 76 | * Find all sections in the course with sectionnum bigger than numsections. 77 | * Either delete these sections or hide them 78 | * 79 | * We will only delete a section if it is completely empty and all sections below 80 | * it are also empty 81 | * 82 | * @param int $courseid 83 | * @param int $numsections 84 | */ 85 | function format_periods_upgrade_hide_extra_sections($courseid, $numsections) { 86 | global $DB; 87 | $sections = $DB->get_records_sql('SELECT id, name, summary, sequence, visible 88 | FROM {course_sections} 89 | WHERE course = ? AND section > ? 90 | ORDER BY section DESC', [$courseid, $numsections]); 91 | $candelete = true; 92 | $tohide = []; 93 | $todelete = []; 94 | foreach ($sections as $section) { 95 | if ($candelete && (!empty($section->summary) || !empty($section->sequence) || !empty($section->name))) { 96 | $candelete = false; 97 | } 98 | if ($candelete) { 99 | $todelete[] = $section->id; 100 | } else if ($section->visible) { 101 | $tohide[] = $section->id; 102 | } 103 | } 104 | if ($todelete) { 105 | // Delete empty sections in the end. 106 | // This is an upgrade script - no events or cache resets are needed. 107 | // We also know that these sections do not have any modules so it is safe to just delete records in the table. 108 | $DB->delete_records_list('course_sections', 'id', $todelete); 109 | } 110 | if ($tohide) { 111 | // Hide other orphaned sections. 112 | // This is different from what set_section_visible() does but we want to preserve actual 113 | // module visibility in this case. 114 | list($sql, $params) = $DB->get_in_or_equal($tohide); 115 | $DB->execute("UPDATE {course_sections} SET visible = 0 WHERE id " . $sql, $params); 116 | } 117 | } 118 | 119 | /** 120 | * Set 'automaticenddate' for existing courses. 121 | */ 122 | function format_periods_upgrade_automaticenddate() { 123 | global $DB, $CFG; 124 | require_once($CFG->dirroot . '/course/format/periods/lib.php'); 125 | 126 | // Go through the existing courses using the periods format with no value set for the 'automaticenddate'. 127 | $sql = "SELECT c.id, c.enddate, cfo.id as cfoid 128 | FROM {course} c 129 | LEFT JOIN {course_format_options} cfo 130 | ON cfo.courseid = c.id 131 | AND cfo.format = c.format 132 | AND cfo.name = :optionname 133 | AND cfo.sectionid = 0 134 | WHERE c.format = :format 135 | AND cfo.id IS NULL"; 136 | $params = ['optionname' => 'automaticenddate', 'format' => 'periods']; 137 | $courses = $DB->get_recordset_sql($sql, $params); 138 | foreach ($courses as $course) { 139 | $option = new stdClass(); 140 | $option->courseid = $course->id; 141 | $option->format = 'periods'; 142 | $option->sectionid = 0; 143 | $option->name = 'automaticenddate'; 144 | if (empty($course->enddate)) { 145 | $option->value = 1; 146 | $DB->insert_record('course_format_options', $option); 147 | 148 | // Now, let's update the course end date. 149 | format_periods::update_end_date($course->id); 150 | } else { 151 | $option->value = 0; 152 | $DB->insert_record('course_format_options', $option); 153 | } 154 | } 155 | $courses->close(); 156 | 157 | } -------------------------------------------------------------------------------- /tests/observer_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Unit tests for the event observers used by the periods course format. 19 | * 20 | * @package format_periods 21 | * @copyright 2017 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | /** 28 | * Unit tests for the event observers used by the periods course format. 29 | * 30 | * @package format_periods 31 | * @copyright 2017 Marina Glancy 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class format_periods_observer_testcase extends advanced_testcase { 35 | 36 | /** 37 | * Test setup. 38 | */ 39 | public function setUp() { 40 | $this->resetAfterTest(); 41 | } 42 | 43 | /** 44 | * Tests when we update a course with automatic end date set. 45 | */ 46 | public function test_course_updated_with_automatic_end_date() { 47 | global $DB; 48 | 49 | // Generate a course with some sections. 50 | $numsections = 6; 51 | $startdate = time(); 52 | $course = $this->getDataGenerator()->create_course(array( 53 | 'numsections' => $numsections, 54 | 'format' => 'periods', 55 | 'startdate' => $startdate, 56 | 'automaticenddate' => 1)); 57 | 58 | // Ok, let's update the course start date. 59 | $newstartdate = $startdate + WEEKSECS; 60 | update_course((object)['id' => $course->id, 'startdate' => $newstartdate]); 61 | 62 | // Get the updated course end date. 63 | $enddate = $DB->get_field('course', 'enddate', array('id' => $course->id)); 64 | 65 | $format = course_get_format($course->id); 66 | $this->assertEquals($numsections, $format->get_last_section_number()); 67 | $this->assertEquals($newstartdate, $format->get_course()->startdate); 68 | $dates = $format->get_section_dates($numsections); 69 | $this->assertEquals($dates->end, $enddate); 70 | } 71 | 72 | /** 73 | * Tests when we update a course with automatic end date set but no actual change is made. 74 | */ 75 | public function test_course_updated_with_automatic_end_date_no_change() { 76 | global $DB; 77 | 78 | // Generate a course with some sections. 79 | $course = $this->getDataGenerator()->create_course(array( 80 | 'numsections' => 6, 81 | 'format' => 'periods', 82 | 'startdate' => time(), 83 | 'automaticenddate' => 1)); 84 | 85 | // Get the end date from the DB as the results will have changed from $course above after observer processing. 86 | $createenddate = $DB->get_field('course', 'enddate', array('id' => $course->id)); 87 | 88 | // Ok, let's update the course - but actually not change anything. 89 | update_course((object)['id' => $course->id]); 90 | 91 | // Get the updated course end date. 92 | $updateenddate = $DB->get_field('course', 'enddate', array('id' => $course->id)); 93 | 94 | // Confirm nothing changed. 95 | $this->assertEquals($createenddate, $updateenddate); 96 | } 97 | 98 | /** 99 | * Tests when we update a course without automatic end date set. 100 | */ 101 | public function test_course_updated_without_automatic_end_date() { 102 | global $DB; 103 | 104 | // Generate a course with some sections. 105 | $startdate = time(); 106 | $enddate = $startdate + WEEKSECS; 107 | $course = $this->getDataGenerator()->create_course(array( 108 | 'numsections' => 6, 109 | 'format' => 'periods', 110 | 'startdate' => $startdate, 111 | 'enddate' => $enddate, 112 | 'automaticenddate' => 0)); 113 | 114 | // Ok, let's update the course start date. 115 | $newstartdate = $startdate + WEEKSECS; 116 | update_course((object)['id' => $course->id, 'startdate' => $newstartdate]); 117 | 118 | // Get the updated course end date. 119 | $updateenddate = $DB->get_field('course', 'enddate', array('id' => $course->id)); 120 | 121 | // Confirm nothing changed. 122 | $this->assertEquals($enddate, $updateenddate); 123 | } 124 | 125 | /** 126 | * Tests when we adding a course section with automatic end date set. 127 | */ 128 | public function test_course_section_created_with_automatic_end_date() { 129 | global $DB; 130 | 131 | $numsections = 6; 132 | $course = $this->getDataGenerator()->create_course(array( 133 | 'numsections' => $numsections, 134 | 'format' => 'periods', 135 | 'startdate' => time(), 136 | 'automaticenddate' => 1)); 137 | 138 | // Add a section to the course. 139 | course_create_section($course->id); 140 | 141 | // Get the updated course end date. 142 | $enddate = $DB->get_field('course', 'enddate', array('id' => $course->id)); 143 | 144 | $format = course_get_format($course->id); 145 | $dates = $format->get_section_dates($numsections + 1); 146 | 147 | // Confirm end date was updated. 148 | $this->assertEquals($enddate, $dates->end); 149 | } 150 | 151 | /** 152 | * Tests when we deleting a course section with automatic end date set. 153 | */ 154 | public function test_course_section_deleted_with_automatic_end_date() { 155 | global $DB; 156 | 157 | // Generate a course with some sections. 158 | $numsections = 6; 159 | $course = $this->getDataGenerator()->create_course(array( 160 | 'numsections' => $numsections, 161 | 'format' => 'periods', 162 | 'startdate' => time(), 163 | 'automaticenddate' => 1)); 164 | 165 | // Add a section to the course. 166 | course_delete_section($course, $numsections); 167 | 168 | // Get the updated course end date. 169 | $enddate = $DB->get_field('course', 'enddate', array('id' => $course->id)); 170 | 171 | $format = course_get_format($course->id); 172 | $dates = $format->get_section_dates($numsections - 1); 173 | 174 | // Confirm end date was updated. 175 | $this->assertEquals($enddate, $dates->end); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /backup/moodle2/restore_format_periods_plugin.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Specialised restore for format_periods 19 | * 20 | * @package format_periods 21 | * @category backup 22 | * @copyright 2017 Marina Glancy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | /** 29 | * Specialised restore for format_periods 30 | * 31 | * Processes 'numsections' from the old backup files and hides sections that used to be "orphaned" 32 | * 33 | * @package format_periods 34 | * @category backup 35 | * @copyright 2017 Marina Glancy 36 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 | */ 38 | class restore_format_periods_plugin extends restore_format_plugin { 39 | 40 | /** @var int */ 41 | protected $originalnumsections = 0; 42 | 43 | /** 44 | * Checks if backup file was made on Moodle before 3.3 and we should respect the 'numsections' 45 | * and potential "orphaned" sections in the end of the course. 46 | * 47 | * @return bool 48 | */ 49 | protected function is_pre_33_backup() { 50 | $backupinfo = $this->step->get_task()->get_info(); 51 | $backuprelease = $backupinfo->backup_release; 52 | return version_compare($backuprelease, '3.3', 'lt'); 53 | } 54 | 55 | /** 56 | * Handles setting the automatic end date for a restored course. 57 | * 58 | * @param int $enddate The end date in the backup file. 59 | */ 60 | protected function update_automatic_end_date($enddate) { 61 | global $DB; 62 | 63 | // At this stage the 'course_format_options' table will already have a value set for this option as it is 64 | // part of the course format and the default will have been set. 65 | // Get the current course format option. 66 | $params = array( 67 | 'courseid' => $this->step->get_task()->get_courseid(), 68 | 'format' => 'periods', 69 | 'sectionid' => 0, 70 | 'name' => 'automaticenddate' 71 | ); 72 | $cfoid = $DB->get_field('course_format_options', 'id', $params); 73 | 74 | $update = new stdClass(); 75 | $update->id = $cfoid; 76 | if (empty($enddate)) { 77 | $update->value = 1; 78 | $DB->update_record('course_format_options', $update); 79 | 80 | // Now, let's update the course end date. 81 | format_periods::update_end_date($this->step->get_task()->get_courseid()); 82 | } else { 83 | $update->value = 0; 84 | $DB->update_record('course_format_options', $update); 85 | } 86 | } 87 | 88 | /** 89 | * Handles updating the visibility of sections in the restored course. 90 | * 91 | * @param int $numsections The number of sections in the restored course. 92 | */ 93 | protected function update_course_sections_visibility($numsections) { 94 | global $DB; 95 | 96 | $backupinfo = $this->step->get_task()->get_info(); 97 | foreach ($backupinfo->sections as $key => $section) { 98 | // For each section from the backup file check if it was restored and if was "orphaned" in the original 99 | // course and mark it as hidden. This will leave all activities in it visible and available just as it was 100 | // in the original course. 101 | // Exception is when we restore with merging and the course already had a section with this section number, 102 | // in this case we don't modify the visibility. 103 | if ($this->step->get_task()->get_setting_value($key . '_included')) { 104 | $sectionnum = (int)$section->title; 105 | if ($sectionnum > $numsections && $sectionnum > $this->originalnumsections) { 106 | $DB->execute("UPDATE {course_sections} SET visible = 0 WHERE course = ? AND section = ?", 107 | [$this->step->get_task()->get_courseid(), $sectionnum]); 108 | } 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * Creates a dummy path element in order to be able to execute code after restore 115 | * 116 | * @return restore_path_element[] 117 | */ 118 | public function define_course_plugin_structure() { 119 | global $DB; 120 | 121 | // Since this method is executed before the restore we can do some pre-checks here. 122 | // In case of merging backup into existing course find the current number of sections. 123 | $target = $this->step->get_task()->get_target(); 124 | if (($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING) && 125 | $this->is_pre_33_backup()) { 126 | $maxsection = $DB->get_field_sql( 127 | 'SELECT max(section) FROM {course_sections} WHERE course = ?', 128 | [$this->step->get_task()->get_courseid()]); 129 | $this->originalnumsections = (int)$maxsection; 130 | } 131 | 132 | // Dummy path element is needed in order for after_restore_course() to be called. 133 | return [new restore_path_element('dummy_course', $this->get_pathfor('/dummycourse'))]; 134 | } 135 | 136 | /** 137 | * Dummy process method 138 | */ 139 | public function process_dummy_course() { 140 | 141 | } 142 | 143 | /** 144 | * Executed after course restore is complete 145 | * 146 | * This method is only executed if course configuration was overridden 147 | */ 148 | public function after_restore_course() { 149 | if (!$this->is_pre_33_backup()) { 150 | // Backup file was made in Moodle 3.3 or later, we don't need to process it. 151 | return; 152 | } 153 | 154 | $backupinfo = $this->step->get_task()->get_info(); 155 | if ($backupinfo->original_course_format !== 'periods') { 156 | // Backup from another course format. 157 | return; 158 | } 159 | 160 | $data = $this->connectionpoint->get_data(); 161 | 162 | // Set the automatic end date setting and the course end date (if applicable). 163 | $this->update_automatic_end_date($data['tags']['enddate']); 164 | 165 | if (isset($data['tags']['numsections'])) { 166 | // Update course sections visibility. 167 | $numsections = (int)$data['tags']['numsections']; 168 | $this->update_course_sections_visibility($numsections); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tests/format_periods_upgrade_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * format_periods unit tests for upgradelib 19 | * 20 | * @package format_periods 21 | * @copyright 2017 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | global $CFG; 28 | require_once($CFG->dirroot . '/course/lib.php'); 29 | require_once($CFG->dirroot . '/course/format/periods/db/upgradelib.php'); 30 | 31 | /** 32 | * format_periods unit tests for upgradelib 33 | * 34 | * @package format_periods 35 | * @copyright 2017 Marina Glancy 36 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 | */ 38 | class format_periods_upgrade_testcase extends advanced_testcase { 39 | 40 | /** 41 | * Test upgrade step to remove orphaned sections. 42 | */ 43 | public function test_numsections_no_actions() { 44 | global $DB; 45 | 46 | $this->resetAfterTest(true); 47 | 48 | $params = array('format' => 'periods', 'numsections' => 5, 'startdate' => 1445644800); 49 | $course = $this->getDataGenerator()->create_course($params); 50 | // This test is executed after 'numsections' option was already removed, add it manually. 51 | $DB->insert_record('course_format_options', ['courseid' => $course->id, 'format' => 'periods', 52 | 'sectionid' => 0, 'name' => 'numsections', 'value' => '5']); 53 | 54 | // There are 6 sections in the course (0-section and sections 1, ... 5). 55 | $this->assertEquals(6, $DB->count_records('course_sections', ['course' => $course->id])); 56 | 57 | format_periods_upgrade_remove_numsections(); 58 | 59 | // There are still 6 sections in the course. 60 | $this->assertEquals(6, $DB->count_records('course_sections', ['course' => $course->id])); 61 | 62 | } 63 | 64 | /** 65 | * Test upgrade step to remove orphaned sections. 66 | */ 67 | public function test_numsections_delete_empty() { 68 | global $DB; 69 | 70 | $this->resetAfterTest(true); 71 | 72 | // Set default number of sections to 10. 73 | set_config('numsections', 10, 'moodlecourse'); 74 | 75 | $params1 = array('format' => 'periods', 'numsections' => 5, 'startdate' => 1445644800); 76 | $course1 = $this->getDataGenerator()->create_course($params1); 77 | $params2 = array('format' => 'periods', 'numsections' => 20, 'startdate' => 1445644800); 78 | $course2 = $this->getDataGenerator()->create_course($params2); 79 | // This test is executed after 'numsections' option was already removed, add it manually and 80 | // set it to be 2 less than actual number of sections. 81 | $DB->insert_record('course_format_options', ['courseid' => $course1->id, 'format' => 'periods', 82 | 'sectionid' => 0, 'name' => 'numsections', 'value' => '3']); 83 | 84 | // There are 6 sections in the first course (0-section and sections 1, ... 5). 85 | $this->assertEquals(6, $DB->count_records('course_sections', ['course' => $course1->id])); 86 | // There are 21 sections in the second course. 87 | $this->assertEquals(21, $DB->count_records('course_sections', ['course' => $course2->id])); 88 | 89 | format_periods_upgrade_remove_numsections(); 90 | 91 | // Two sections were deleted in the first course. 92 | $this->assertEquals(4, $DB->count_records('course_sections', ['course' => $course1->id])); 93 | // The second course was reset to 11 sections (default plus 0-section). 94 | $this->assertEquals(11, $DB->count_records('course_sections', ['course' => $course2->id])); 95 | 96 | } 97 | 98 | /** 99 | * Test upgrade step to remove orphaned sections. 100 | */ 101 | public function test_numsections_hide_non_empty() { 102 | global $DB; 103 | 104 | $this->resetAfterTest(true); 105 | 106 | $params = array('format' => 'periods', 'numsections' => 5, 'startdate' => 1445644800); 107 | $course = $this->getDataGenerator()->create_course($params); 108 | 109 | // Add a module to the second last section. 110 | $cm = $this->getDataGenerator()->create_module('forum', ['course' => $course->id, 'section' => 4]); 111 | 112 | // This test is executed after 'numsections' option was already removed, add it manually and 113 | // set it to be 2 less than actual number of sections. 114 | $DB->insert_record('course_format_options', ['courseid' => $course->id, 'format' => 'periods', 115 | 'sectionid' => 0, 'name' => 'numsections', 'value' => '3']); 116 | 117 | // There are 6 sections. 118 | $this->assertEquals(6, $DB->count_records('course_sections', ['course' => $course->id])); 119 | 120 | format_periods_upgrade_remove_numsections(); 121 | 122 | // One section was deleted and one hidden. 123 | $this->assertEquals(5, $DB->count_records('course_sections', ['course' => $course->id])); 124 | $this->assertEquals(0, $DB->get_field('course_sections', 'visible', ['course' => $course->id, 'section' => 4])); 125 | // The module is still visible. 126 | $this->assertEquals(1, $DB->get_field('course_modules', 'visible', ['id' => $cm->cmid])); 127 | } 128 | 129 | public function test_upgrade_automaticenddate() { 130 | global $DB; 131 | 132 | $this->resetAfterTest(true); 133 | 134 | $params = array('format' => 'periods', 'numsections' => 5, 'startdate' => 1445644800); 135 | $course1 = $this->getDataGenerator()->create_course($params); 136 | $course2 = $this->getDataGenerator()->create_course($params); 137 | 138 | // Remove the option to pretend we are on 3.2. 139 | $DB->delete_records('course_format_options', ['name' => 'automaticenddate', 'format' => 'periods']); 140 | 141 | // Set end date to something in course1 and to 0 in course2. Perform upgrade. 142 | $DB->set_field('course', 'enddate', $params['startdate'] + YEARSECS, ['id' => $course1->id]); 143 | $DB->set_field('course', 'enddate', 0, ['id' => $course2->id]); 144 | format_periods_upgrade_automaticenddate(); 145 | 146 | // For course1 (with enddate) automaticenddate is 0 and enddate is preserved. 147 | $courseformat1 = course_get_format($course1->id); 148 | $this->assertEquals(0, $courseformat1->get_course()->automaticenddate); 149 | $this->assertEquals($params['startdate'] + YEARSECS, $courseformat1->get_course()->enddate); 150 | 151 | // For course2 (without enddate) automaticenddate is 1 and enddate is calculated. 152 | $courseformat2 = course_get_format($course2->id); 153 | $this->assertEquals(1, $courseformat2->get_course()->automaticenddate); 154 | $this->assertEquals($params['startdate'] + 5 * WEEKSECS, $courseformat2->get_course()->enddate); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /tests/format_periods_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * format_periods related unit tests 19 | * 20 | * @package format_periods 21 | * @copyright 2017 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | global $CFG; 28 | require_once($CFG->dirroot . '/course/lib.php'); 29 | 30 | /** 31 | * format_periods related unit tests 32 | * 33 | * @package format_periods 34 | * @copyright 2015 Marina Glancy 35 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 | */ 37 | class format_periods_testcase extends advanced_testcase { 38 | 39 | /** 40 | * Tests for format_periods::get_section_name method with default section names. 41 | */ 42 | public function test_get_section_name() { 43 | global $DB; 44 | $this->resetAfterTest(true); 45 | 46 | // Generate a course with 5 sections. 47 | $generator = $this->getDataGenerator(); 48 | $numsections = 5; 49 | $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'periods'), 50 | array('createsections' => true)); 51 | 52 | // Get section names for course. 53 | $coursesections = $DB->get_records('course_sections', array('course' => $course->id)); 54 | 55 | // Test get_section_name with default section names. 56 | $courseformat = course_get_format($course); 57 | foreach ($coursesections as $section) { 58 | // Assert that with unmodified section names, get_section_name returns the same result as get_default_section_name. 59 | $this->assertEquals($courseformat->get_default_section_name($section), $courseformat->get_section_name($section)); 60 | } 61 | } 62 | 63 | /** 64 | * Tests for format_periods::get_section_name method with modified section names. 65 | */ 66 | public function test_get_section_name_customised() { 67 | global $DB; 68 | $this->resetAfterTest(true); 69 | 70 | // Generate a course with 5 sections. 71 | $generator = $this->getDataGenerator(); 72 | $numsections = 5; 73 | $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'periods'), 74 | array('createsections' => true)); 75 | 76 | // Get section names for course. 77 | $coursesections = $DB->get_records('course_sections', array('course' => $course->id)); 78 | 79 | // Modify section names. 80 | $customname = "Custom Section"; 81 | foreach ($coursesections as $section) { 82 | $section->name = "$customname $section->section"; 83 | $DB->update_record('course_sections', $section); 84 | } 85 | 86 | // Requery updated section names then test get_section_name. 87 | $coursesections = $DB->get_records('course_sections', array('course' => $course->id)); 88 | $courseformat = course_get_format($course); 89 | foreach ($coursesections as $section) { 90 | // Assert that with modified section names, get_section_name returns the modified section name. 91 | $this->assertEquals($section->name, $courseformat->get_section_name($section)); 92 | } 93 | } 94 | 95 | /** 96 | * Tests for format_periods::get_default_section_name. 97 | */ 98 | public function test_get_default_section_name() { 99 | global $DB; 100 | $this->resetAfterTest(true); 101 | 102 | // Generate a course with 5 sections. 103 | $generator = $this->getDataGenerator(); 104 | $numsections = 5; 105 | $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'periods'), 106 | array('createsections' => true)); 107 | 108 | // Get section names for course. 109 | $coursesections = $DB->get_records('course_sections', array('course' => $course->id)); 110 | 111 | // Test get_default_section_name with default section names. 112 | $courseformat = course_get_format($course); 113 | foreach ($coursesections as $section) { 114 | if ($section->section == 0) { 115 | $sectionname = get_string('section0name', 'format_periods'); 116 | $this->assertEquals($sectionname, $courseformat->get_default_section_name($section)); 117 | } else { 118 | $dates = $courseformat->get_section_dates($section); 119 | $dates->end = ($dates->end); 120 | $dateformat = get_string('strftimedateshort', 'langconfig'); 121 | $weekday = userdate($dates->start, $dateformat); 122 | $endweekday = userdate($dates->end - 1, $dateformat); 123 | $sectionname = $weekday.' - '.$endweekday; 124 | 125 | $this->assertEquals($sectionname, $courseformat->get_default_section_name($section)); 126 | } 127 | } 128 | } 129 | 130 | /** 131 | * Test web service updating section name 132 | */ 133 | public function test_update_inplace_editable() { 134 | global $CFG, $DB, $PAGE; 135 | require_once($CFG->dirroot . '/lib/external/externallib.php'); 136 | 137 | $this->resetAfterTest(); 138 | $user = $this->getDataGenerator()->create_user(); 139 | $this->setUser($user); 140 | $course = $this->getDataGenerator()->create_course(array('numsections' => 5, 'format' => 'periods'), 141 | array('createsections' => true)); 142 | $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 2)); 143 | 144 | // Call webservice without necessary permissions. 145 | try { 146 | core_external::update_inplace_editable('format_periods', 'sectionname', $section->id, 'New section name'); 147 | $this->fail('Exception expected'); 148 | } catch (moodle_exception $e) { 149 | $this->assertEquals('Course or activity not accessible. (Not enrolled)', 150 | $e->getMessage()); 151 | } 152 | 153 | // Change to teacher and make sure that section name can be updated using web service update_inplace_editable(). 154 | $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 155 | $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id); 156 | 157 | $res = core_external::update_inplace_editable('format_periods', 'sectionname', $section->id, 'New section name'); 158 | $res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res); 159 | $this->assertEquals('New section name', $res['value']); 160 | $this->assertEquals('New section name', $DB->get_field('course_sections', 'name', array('id' => $section->id))); 161 | } 162 | 163 | /** 164 | * Test callback updating section name 165 | */ 166 | public function test_inplace_editable() { 167 | global $CFG, $DB, $PAGE; 168 | 169 | $this->resetAfterTest(); 170 | $user = $this->getDataGenerator()->create_user(); 171 | $course = $this->getDataGenerator()->create_course(array('numsections' => 5, 'format' => 'periods'), 172 | array('createsections' => true)); 173 | $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 174 | $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id); 175 | $this->setUser($user); 176 | 177 | $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 2)); 178 | 179 | // Call callback format_periods_inplace_editable() directly. 180 | $tmpl = component_callback('format_periods', 'inplace_editable', array('sectionname', $section->id, 'Rename me again')); 181 | $this->assertInstanceOf('core\output\inplace_editable', $tmpl); 182 | $res = $tmpl->export_for_template($PAGE->get_renderer('core')); 183 | $this->assertEquals('Rename me again', $res['value']); 184 | $this->assertEquals('Rename me again', $DB->get_field('course_sections', 'name', array('id' => $section->id))); 185 | 186 | // Try updating using callback from mismatching course format. 187 | try { 188 | $tmpl = component_callback('format_topics', 'inplace_editable', array('sectionname', $section->id, 'New name')); 189 | $this->fail('Exception expected'); 190 | } catch (moodle_exception $e) { 191 | $this->assertEquals(1, preg_match('/^Can not find data record in database/', $e->getMessage())); 192 | } 193 | } 194 | 195 | /** 196 | * Test get_default_course_enddate. 197 | * 198 | * @return void 199 | */ 200 | public function test_default_course_enddate() { 201 | global $CFG, $DB, $PAGE; 202 | 203 | $this->resetAfterTest(true); 204 | 205 | require_once($CFG->dirroot . '/course/tests/fixtures/testable_course_edit_form.php'); 206 | 207 | $this->setTimezone('UTC'); 208 | 209 | $params = array('format' => 'periods', 'numsections' => 5, 'startdate' => 1445644800); 210 | $course = $this->getDataGenerator()->create_course($params); 211 | $category = $DB->get_record('course_categories', array('id' => $course->category)); 212 | 213 | $args = [ 214 | 'course' => $course, 215 | 'category' => $category, 216 | 'editoroptions' => [ 217 | 'context' => context_course::instance($course->id), 218 | 'subdirs' => 0 219 | ], 220 | 'returnto' => new moodle_url('/'), 221 | 'returnurl' => new moodle_url('/'), 222 | ]; 223 | 224 | $PAGE->set_course($course); 225 | $courseform = new testable_course_edit_form(null, $args); 226 | $courseform->definition_after_data(); 227 | 228 | $enddate = $params['startdate'] + (WEEKSECS * $params['numsections']); 229 | 230 | $periodsformat = course_get_format($course->id); 231 | $this->assertEquals($enddate, $periodsformat->get_default_course_enddate($courseform->get_quick_form())); 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /renderer.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Renderer for outputting the periods course format. 19 | * 20 | * @package format_periods 21 | * @copyright 2014 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | require_once($CFG->dirroot.'/course/format/renderer.php'); 28 | require_once($CFG->dirroot.'/course/format/periods/lib.php'); 29 | 30 | 31 | /** 32 | * Basic renderer for periods format. 33 | * 34 | * @copyright 2014 Marina Glancy 35 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 | */ 37 | class format_periods_renderer extends format_section_renderer_base { 38 | /** 39 | * Generate the starting container html for a list of sections 40 | * @return string HTML to output. 41 | */ 42 | protected function start_section_list() { 43 | return html_writer::start_tag('ul', array('class' => 'periods')); 44 | } 45 | 46 | /** 47 | * Generate the closing container html for a list of sections 48 | * @return string HTML to output. 49 | */ 50 | protected function end_section_list() { 51 | return html_writer::end_tag('ul'); 52 | } 53 | 54 | /** 55 | * Generate the title for this section page 56 | * @return string the page title 57 | */ 58 | protected function page_title() { 59 | return get_string('weeklyoutline'); 60 | } 61 | 62 | /** 63 | * Output the html for a multiple section page 64 | * 65 | * @param stdClass $course The course entry from DB 66 | * @param array $sections (argument not used) 67 | * @param array $mods (argument not used) 68 | * @param array $modnames (argument not used) 69 | * @param array $modnamesused (argument not used) 70 | */ 71 | public function print_multiple_section_page($course, $sections, $mods, $modnames, $modnamesused) { 72 | global $PAGE; 73 | 74 | $modinfo = get_fast_modinfo($course); 75 | $course = course_get_format($course)->get_course(); 76 | 77 | $context = context_course::instance($course->id); 78 | // Title with completion help icon. 79 | $completioninfo = new completion_info($course); 80 | echo $completioninfo->display_help_icon(); 81 | echo $this->output->heading($this->page_title(), 2, 'accesshide'); 82 | 83 | // Copy activity clipboard.. 84 | echo $this->course_activity_clipboard($course, 0); 85 | 86 | // Now the list of sections.. 87 | echo $this->start_section_list(); 88 | 89 | foreach ($modinfo->get_section_info_all() as $section => $thissection) { 90 | if ($section == 0) { 91 | // 0-section is displayed a little different then the others. 92 | if ($thissection->summary or !empty($modinfo->sections[0]) or $PAGE->user_is_editing()) { 93 | echo $this->section_header($thissection, $course, false, 0); 94 | echo $this->courserenderer->course_section_cm_list($course, $thissection, 0); 95 | echo $this->courserenderer->course_section_add_cm_control($course, 0, 0); 96 | echo $this->section_footer(); 97 | } 98 | continue; 99 | } 100 | 101 | // Do not display sections in the past/future that must be hidden by course settings. 102 | $displaymode = course_get_format($course)->get_section_display_mode($thissection); 103 | if (!has_capability('moodle/course:viewhiddenactivities', $context)) { 104 | if ($displaymode == FORMAT_PERIODS_NOTAVAILABLE) { 105 | echo $this->section_hidden($section, $course->id); 106 | continue; 107 | } 108 | if ($displaymode == FORMAT_PERIODS_NOTDISPLAYED || $displaymode == FORMAT_PERIODS_HIDDEN) { 109 | continue; 110 | } 111 | } 112 | 113 | // Show the section if the user is permitted to access it, OR if it's not available 114 | // but there is some available info text which explains the reason & should display. 115 | $showsection = $thissection->uservisible || 116 | ($thissection->visible && !$thissection->available && 117 | !empty($thissection->availableinfo)); 118 | if (!$showsection) { 119 | // If the hiddensections option is set to 'show hidden sections in collapsed 120 | // form', then display the hidden section message - UNLESS the section is 121 | // hidden by the availability system, which is set to hide the reason. 122 | if (!$course->hiddensections && $thissection->available) { 123 | echo $this->section_hidden($section, $course->id); 124 | } 125 | 126 | continue; 127 | } 128 | 129 | if (!$PAGE->user_is_editing() && 130 | $displaymode == FORMAT_PERIODS_COLLAPSED) { 131 | // Display section summary only. 132 | echo $this->section_summary($thissection, $course, null); 133 | } else { 134 | echo $this->section_header($thissection, $course, false, 0); 135 | if ($thissection->uservisible) { 136 | echo $this->courserenderer->course_section_cm_list($course, $thissection, 0); 137 | echo $this->courserenderer->course_section_add_cm_control($course, $section, 0); 138 | } 139 | echo $this->section_footer(); 140 | } 141 | } 142 | 143 | echo $this->end_section_list(); 144 | 145 | if ($PAGE->user_is_editing() and has_capability('moodle/course:update', $context)) { 146 | echo $this->change_number_sections($course, 0); 147 | } 148 | 149 | } 150 | 151 | /** 152 | * Returns sections dates inteval as a human-readable string 153 | * 154 | * @param int|stdClass $section either section number (field course_section.section) or row from course_section table 155 | * @return string 156 | */ 157 | protected function section_dates($section) { 158 | $courseformat = course_get_format($section->course); 159 | $section = $courseformat->get_section($section); 160 | $context = context_course::instance($section->course); 161 | if (has_capability('moodle/course:update', $context)) { 162 | $defaultduration = $courseformat->get_course()->periodduration; 163 | $o = array( 164 | 'dates' => $courseformat->get_default_section_name($section), 165 | 'duration' => $section->periodduration ? $section->periodduration : $defaultduration 166 | ); 167 | $o['duration'] = $this->duration_to_string($o['duration']); 168 | if (!empty($section->name)) { 169 | if (!empty($section->periodduration) && $section->periodduration != $defaultduration) { 170 | $string = 'sectiondatesduration'; 171 | } else { 172 | $string = 'sectiondates'; 173 | } 174 | } else if ($section->periodduration && $section->periodduration != $defaultduration) { 175 | $string = 'sectionduration'; 176 | } else { 177 | return ''; 178 | } 179 | $text = get_string($string, 'format_periods', (object)$o); 180 | return html_writer::tag('div', $text, array('class' => 'sectiondates')); 181 | } 182 | return ''; 183 | } 184 | 185 | /** 186 | * Converts a duration (in the format 'NN UNIT') into a localised language string 187 | * (e.g. '4 week' => '4 Wochen') 188 | * 189 | * @param string $duration 190 | * @return string 191 | */ 192 | protected function duration_to_string($duration) { 193 | if (!preg_match('/^(\d+) (\w+)$/', $duration, $matches)) { 194 | return $duration; 195 | } 196 | $num = (int)$matches[1]; 197 | $units = $matches[2]; 198 | if ($num > 1) { 199 | $units .= 's'; 200 | } 201 | return get_string('num'.$units, 'core', $num); 202 | } 203 | 204 | /** 205 | * Generate html for a section summary text 206 | * 207 | * @param stdClass $section The course_section entry from DB 208 | * @return string HTML to output. 209 | */ 210 | protected function format_summary_text($section) { 211 | $context = context_course::instance($section->course); 212 | $summarytext = $this->section_dates($section). file_rewrite_pluginfile_urls($section->summary, 'pluginfile.php', 213 | $context->id, 'course', 'section', $section->id); 214 | 215 | $options = new stdClass(); 216 | $options->noclean = true; 217 | $options->overflowdiv = true; 218 | return format_text($summarytext, $section->summaryformat, $options); 219 | } 220 | 221 | /** 222 | * Generate the section title, wraps it in a link to the section page if page is to be displayed on a separate page 223 | * 224 | * @param stdClass $section The course_section entry from DB 225 | * @param stdClass $course The course entry from DB 226 | * @return string HTML to output. 227 | */ 228 | public function section_title($section, $course) { 229 | global $CFG; 230 | if ((float)$CFG->version >= 2016052300) { 231 | // For Moodle 3.1 and later use inplace editable section name. 232 | return $this->render(course_get_format($course)->inplace_editable_render_section_name($section)); 233 | } 234 | return parent::section_title($section, $course); 235 | } 236 | 237 | /** 238 | * Generate the section title to be displayed on the section page, without a link 239 | * 240 | * This method is only invoked in Moodle versions 3.1 and later. 241 | * 242 | * @param stdClass $section The course_section entry from DB 243 | * @param stdClass $course The course entry from DB 244 | * @return string HTML to output. 245 | */ 246 | public function section_title_without_link($section, $course) { 247 | return $this->render(course_get_format($course)->inplace_editable_render_section_name($section, false)); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /periodduration.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Duration form element 19 | * 20 | * Contains class to create length of time for element. 21 | * 22 | * @package format_periods 23 | * @copyright 2015 Marina Glancy 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | 27 | global $CFG; 28 | require_once($CFG->libdir . '/form/group.php'); 29 | require_once($CFG->libdir . '/formslib.php'); 30 | require_once($CFG->libdir . '/form/text.php'); 31 | 32 | MoodleQuickForm::registerElementType('periodduration', "$CFG->dirroot/course/format/periods/periodduration.php", 33 | 'format_periods_periodduration'); 34 | 35 | /** 36 | * Period duration element 37 | * 38 | * HTML class for a length of days/weeks/months. 39 | * The values returned to PHP as string to use in strtotime(), for example 40 | * '1 day', '2 week', '3 month', etc.. 41 | * 42 | * @package format_periods 43 | * @copyright 2015 Marina Glancy 44 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 45 | */ 46 | class format_periods_periodduration extends MoodleQuickForm_group { 47 | /** 48 | * Control the fieldnames for form elements 49 | * optional => if true, show a checkbox beside the element to turn it on (or off) 50 | * @var array 51 | */ 52 | protected $_options = array('optional' => false, 'default' => '1 week', 'defaultunit' => 'week'); 53 | 54 | /** @var array associative array of time units (days, hours, minutes, seconds) */ 55 | private $_units = null; 56 | 57 | /** 58 | * constructor 59 | * 60 | * @param string $elementname Element's name 61 | * @param mixed $elementlabel Label(s) for an element 62 | * @param array $options Options to control the element's display. Recognised values are 63 | * 'optional' => true/false - whether to display an 'enabled' checkbox next to the element. 64 | * 'defaultunit' => day, week, month, year - the default unit to display when the time is blank. 65 | * 'defaulttime' => the default number of units to display when the time is blank 66 | * If not specified, minutes is used. 67 | * @param mixed $attributes Either a typical HTML attribute string or an associative array 68 | */ 69 | public function __construct($elementname = null, $elementlabel = null, $options = array(), $attributes = null) { 70 | parent::__construct($elementname, $elementlabel); 71 | $this->setAttributes($attributes); 72 | $this->_persistantFreeze = true; 73 | $this->_appendName = true; 74 | $this->_type = 'duration'; 75 | 76 | // Set the options, do not bother setting bogus ones. 77 | if (!is_array($options)) { 78 | $options = array(); 79 | } 80 | $this->_options['optional'] = !empty($options['optional']); 81 | if (isset($options['defaultunit'])) { 82 | if (!array_key_exists($options['defaultunit'], $this->get_units())) { 83 | throw new coding_exception($options['defaultunit'] . 84 | ' is not a recognised unit in format_periods_periodduration.'); 85 | } 86 | $this->_options['defaultunit'] = $options['defaultunit']; 87 | } 88 | if (array_key_exists('default', $options)) { 89 | $this->_options['default'] = $options['default']; 90 | } 91 | } 92 | 93 | /** 94 | * Returns time associative array of unit length. 95 | * 96 | * @return array unit length in seconds => string unit name. 97 | */ 98 | public function get_units() { 99 | if (is_null($this->_units)) { 100 | $this->_units = array( 101 | 'day' => get_string('numdays', 'moodle', ''), 102 | 'week' => get_string('numweeks', 'moodle', ''), 103 | 'month' => get_string('nummonths', 'moodle', ''), 104 | 'year' => get_string('numyears', 'moodle', ''), 105 | ); 106 | } 107 | return $this->_units; 108 | } 109 | 110 | /** 111 | * Converts value to the best possible time unit. for example 112 | * '2 week' -> array(2, 'week') 113 | * 114 | * @param string $value an amout of time in seconds or text value (i.e. '2 week') 115 | * @return array associative array ($number => $unit) 116 | */ 117 | public function value_to_unit($value) { 118 | if (preg_match('/^(\d+) (\w+)$/', $value, $matches) && 119 | array_key_exists($matches[2], $this->get_units())) { 120 | return array((int)$matches[1], $matches[2]); 121 | } 122 | if (is_int($value)) { 123 | if (is_int($value / WEEKSECS)) { 124 | return array($value / WEEKSECS, 'week'); 125 | } else { 126 | return array((int)($value / DAYSECS), 'day'); 127 | } 128 | } 129 | if (preg_match('/^(\d+) (\w+)$/', $this->_options['default'], $matches) && 130 | array_key_exists($matches[2], $this->get_units())) { 131 | return array((int)$matches[1], $matches[2]); 132 | } 133 | return array(0, $this->_options['defaultunit']); 134 | } 135 | 136 | /** 137 | * Override of standard quickforms method to create this element. 138 | */ 139 | public function _createElements() { 140 | $attributes = $this->getAttributes(); 141 | if (is_null($attributes)) { 142 | $attributes = array(); 143 | } 144 | if (!isset($attributes['size'])) { 145 | $attributes['size'] = 3; 146 | } 147 | $this->_elements = array(); 148 | // E_STRICT creating elements without forms is nasty because it internally uses $this. 149 | $this->_elements[] = $this->createFormElement('text', 'number', get_string('time', 'form'), $attributes, true); 150 | unset($attributes['size']); 151 | $this->_elements[] = $this->createFormElement('select', 'timeunit', 152 | get_string('timeunit', 'form'), $this->get_units(), $attributes, true); 153 | // If optional we add a checkbox which the user can use to turn if on. 154 | if ($this->_options['optional']) { 155 | $this->_elements[] = $this->createFormElement('checkbox', 'enabled', null, 156 | get_string('enable'), $this->getAttributes(), true); 157 | } 158 | foreach ($this->_elements as $element) { 159 | if (method_exists($element, 'setHiddenLabel')) { 160 | $element->setHiddenLabel(true); 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * Called by HTML_QuickForm whenever form event is made on this element 167 | * 168 | * @param string $event Name of event 169 | * @param mixed $arg event arguments 170 | * @param object $caller calling object 171 | * @return bool 172 | */ 173 | public function onQuickFormEvent($event, $arg, &$caller) { 174 | $this->setMoodleForm($caller); 175 | switch ($event) { 176 | case 'updateValue': 177 | // Constant values override both default and submitted ones, 178 | // default values are overriden by submitted. 179 | $value = $this->_findValue($caller->_constantValues); 180 | if (null === $value) { 181 | // If no boxes were checked, then there is no value in the array 182 | // yet we don't want to display default value in this case. 183 | if ($caller->isSubmitted()) { 184 | $value = $this->_findValue($caller->_submitValues); 185 | } else { 186 | $value = $this->_findValue($caller->_defaultValues); 187 | } 188 | } 189 | if (!is_array($value)) { 190 | list($number, $unit) = $this->value_to_unit($value); 191 | $value = array('number' => $number, 'timeunit' => $unit); 192 | // If optional, default to off, unless a date was provided. 193 | if ($this->_options['optional']) { 194 | $value['enabled'] = $number != 0; 195 | } 196 | } else { 197 | $value['enabled'] = isset($value['enabled']); 198 | } 199 | if (null !== $value) { 200 | $this->setValue($value); 201 | } 202 | break; 203 | 204 | case 'createElement': 205 | if (!empty($arg[2]['optional'])) { 206 | $caller->disabledIf($arg[0], $arg[0] . '[enabled]'); 207 | } 208 | $caller->setType($arg[0] . '[number]', PARAM_INT); 209 | return parent::onQuickFormEvent($event, $arg, $caller); 210 | break; 211 | 212 | default: 213 | return parent::onQuickFormEvent($event, $arg, $caller); 214 | } 215 | } 216 | 217 | /** 218 | * Returns HTML for advchecbox form element. 219 | * 220 | * @return string 221 | */ 222 | public function toHtml() { 223 | include_once('HTML/QuickForm/Renderer/Default.php'); 224 | $renderer = new HTML_QuickForm_Renderer_Default(); 225 | $renderer->setElementTemplate('{element}'); 226 | parent::accept($renderer); 227 | return $renderer->toHtml(); 228 | } 229 | 230 | /** 231 | * Accepts a renderer 232 | * 233 | * @param HTML_QuickForm_Renderer $renderer An HTML_QuickForm_Renderer object 234 | * @param bool $required Whether a group is required 235 | * @param string $error An error message associated with a group 236 | */ 237 | public function accept(&$renderer, $required = false, $error = null) { 238 | $renderer->renderElement($this, $required, $error); 239 | } 240 | 241 | /** 242 | * Output a timestamp. Give it the name of the group. 243 | * Override of standard quickforms method. 244 | * 245 | * @param array $submitvalues 246 | * @param bool $notused Not used. 247 | * @return array field name => value. The value is the time interval in seconds. 248 | */ 249 | public function exportValue(&$submitvalues, $notused = false) { 250 | // Get the values from all the child elements. 251 | $valuearray = array(); 252 | foreach ($this->_elements as $element) { 253 | $thisexport = $element->exportValue($submitvalues[$this->getName()], true); 254 | if (!is_null($thisexport)) { 255 | $valuearray += $thisexport; 256 | } 257 | } 258 | 259 | // Convert the value to an integer number of seconds. 260 | if (empty($valuearray)) { 261 | return null; 262 | } 263 | if ($this->_options['optional'] && empty($valuearray['enabled'])) { 264 | return array($this->getName() => 0); 265 | } 266 | return array($this->getName() => $valuearray['number'] . ' ' . $valuearray['timeunit']); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /lib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * This file contains main class for the course format Periods 19 | * 20 | * @package format_periods 21 | * @copyright 2014 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | require_once($CFG->dirroot. '/course/format/lib.php'); 27 | 28 | define('FORMAT_PERIODS_AS_ABOVE', 0); 29 | 30 | define('FORMAT_PERIODS_EXPANDED', 0); 31 | define('FORMAT_PERIODS_COLLAPSED', 1); 32 | define('FORMAT_PERIODS_NOTDISPLAYED', 2); 33 | define('FORMAT_PERIODS_HIDDEN', 5); 34 | define('FORMAT_PERIODS_NOTAVAILABLE', 6); 35 | 36 | /* UPGRADE SCRIPT: future not available 5->2 */ 37 | 38 | /** 39 | * Main class for the Periods course format 40 | * 41 | * @package format_periods 42 | * @copyright 2014 Marina Glancy 43 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 44 | */ 45 | class format_periods extends format_base { 46 | 47 | /** 48 | * Returns true if this course format uses sections 49 | * 50 | * @return bool 51 | */ 52 | public function uses_sections() { 53 | return true; 54 | } 55 | 56 | /** 57 | * Returns the display name of the given section that the course prefers. 58 | * 59 | * @param int|stdClass $section Section object from database or just field section.section 60 | * @return string Display name that the course format prefers, e.g. "Topic 2" 61 | */ 62 | public function get_section_name($section) { 63 | $section = $this->get_section($section); 64 | if ((string)$section->name !== '') { 65 | // Return the name the user set. 66 | return format_string($section->name, true, array('context' => context_course::instance($this->courseid))); 67 | } else { 68 | return $this->get_default_section_name($section); 69 | } 70 | } 71 | 72 | /** 73 | * Returns the default name for the section (dates interval). 74 | * 75 | * @param int|stdClass|section_info $section 76 | * @return string 77 | */ 78 | public function get_default_section_name($section) { 79 | $section = $this->get_section($section); 80 | if ($section->section == 0) { 81 | // Return the general section. 82 | return get_string('section0name', 'format_periods'); 83 | } 84 | 85 | $dates = $this->get_section_dates($section); 86 | 87 | $course = $this->get_course(); 88 | if (empty($course->datesformat)) { 89 | $dateformat = get_string('strftimedateshort', 'langconfig'); 90 | } else if ($course->datesformat === 'custom') { 91 | $dateformat = $course->datesformatcustom; 92 | } else { 93 | $dateformat = get_string($course->datesformat, 'langconfig'); 94 | } 95 | 96 | $weekday = userdate($dates->start, $dateformat); 97 | $endweekday = userdate($dates->end - 1, $dateformat); 98 | if ($weekday === $endweekday) { 99 | return $weekday; 100 | } else { 101 | return $weekday.' - '.$endweekday; 102 | } 103 | } 104 | 105 | /** 106 | * The URL to use for the specified course (with section) 107 | * 108 | * @param int|stdClass $section Section object from database or just field course_sections.section 109 | * if omitted the course view page is returned 110 | * @param array $options options for view URL. At the moment core uses: 111 | * 'navigation' (bool) if true and section has no separate page, the function returns null 112 | * 'sr' (int) used by multipage formats to specify to which section to return 113 | * @return null|moodle_url 114 | */ 115 | public function get_view_url($section, $options = array()) { 116 | global $CFG; 117 | $course = $this->get_course(); 118 | $url = new moodle_url('/course/view.php', array('id' => $course->id)); 119 | 120 | $sr = null; 121 | if (array_key_exists('sr', $options)) { 122 | $sr = $options['sr']; 123 | } 124 | if (is_object($section)) { 125 | $sectionno = $section->section; 126 | } else { 127 | $sectionno = $section; 128 | } 129 | if ($sectionno !== null) { 130 | if ($sr !== null) { 131 | if ($sr) { 132 | $displaymode = COURSE_DISPLAY_MULTIPAGE; 133 | $sectionno = $sr; 134 | } else { 135 | $displaymode = COURSE_DISPLAY_SINGLEPAGE; 136 | } 137 | } else { 138 | $displaymode = $this->get_section_display_mode($section); 139 | } 140 | if ($sectionno != 0 && $displaymode == COURSE_DISPLAY_MULTIPAGE) { 141 | $url->param('section', $sectionno); 142 | } else { 143 | if (empty($CFG->linkcoursesections) && !empty($options['navigation'])) { 144 | return null; 145 | } 146 | $url->set_anchor('section-'.$sectionno); 147 | } 148 | } 149 | return $url; 150 | } 151 | 152 | /** 153 | * Returns the information about the ajax support in the given source format 154 | * 155 | * The returned object's property (boolean)capable indicates that 156 | * the course format supports Moodle course ajax features. 157 | * 158 | * @return stdClass 159 | */ 160 | public function supports_ajax() { 161 | $ajaxsupport = new stdClass(); 162 | $ajaxsupport->capable = true; 163 | return $ajaxsupport; 164 | } 165 | 166 | /** 167 | * Loads all of the course sections into the navigation 168 | * 169 | * @param global_navigation $navigation 170 | * @param navigation_node $node The course node within the navigation 171 | */ 172 | public function extend_course_navigation($navigation, navigation_node $node) { 173 | global $PAGE; 174 | // If section is specified in course/view.php, make sure it is expanded in navigation. 175 | if ($navigation->includesectionnum === false) { 176 | $selectedsection = optional_param('section', null, PARAM_INT); 177 | if ($selectedsection !== null && (!defined('AJAX_SCRIPT') || AJAX_SCRIPT == '0') && 178 | $PAGE->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) { 179 | $navigation->includesectionnum = $selectedsection; 180 | } 181 | } 182 | parent::extend_course_navigation($navigation, $node); 183 | 184 | $modinfo = get_fast_modinfo($this->get_course()); 185 | $context = context_course::instance($modinfo->courseid); 186 | $sectioninfos = $this->get_sections(); 187 | 188 | foreach ($sectioninfos as $sectionnum => $section) { 189 | if ($sectionnum == 0) { 190 | if (empty($modinfo->sections[0]) && ($sectionnode = $node->get($section->id, navigation_node::TYPE_SECTION))) { 191 | // The general section is empty, remove the node from navigation. 192 | $sectionnode->remove(); 193 | } 194 | } else if (($this->get_section_display_mode($section) > FORMAT_PERIODS_COLLAPSED) && 195 | ($sectionnode = $node->get($section->id, navigation_node::TYPE_SECTION))) { 196 | // Remove or hide navigation nodes for sections that are hidden/not available. 197 | if (!has_capability('moodle/course:viewhiddenactivities', $context) && 198 | $navigation->includesectionnum != $sectionnum) { 199 | $sectionnode->remove(); 200 | } else { 201 | $sectionnode->hidden = true; 202 | } 203 | } 204 | } 205 | } 206 | 207 | /** 208 | * Custom action after section has been moved in AJAX mode 209 | * 210 | * Used in course/rest.php 211 | * 212 | * @return array This will be passed in ajax respose 213 | */ 214 | public function ajax_section_move() { 215 | global $PAGE; 216 | $titles = array(); 217 | $current = -1; 218 | $course = $this->get_course(); 219 | $modinfo = get_fast_modinfo($course); 220 | $renderer = $this->get_renderer($PAGE); 221 | if ($renderer && ($sections = $modinfo->get_section_info_all())) { 222 | foreach ($sections as $number => $section) { 223 | $titles[$number] = $renderer->section_title($section, $course); 224 | if ($this->is_section_current($section)) { 225 | $current = $number; 226 | } 227 | } 228 | } 229 | return array('sectiontitles' => $titles, 'current' => $current, 'action' => 'move'); 230 | } 231 | 232 | /** 233 | * Returns the list of blocks to be automatically added for the newly created course 234 | * 235 | * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT 236 | * each of values is an array of block names (for left and right side columns) 237 | */ 238 | public function get_default_blocks() { 239 | return array( 240 | BLOCK_POS_LEFT => array(), 241 | BLOCK_POS_RIGHT => array() 242 | ); 243 | } 244 | 245 | /** 246 | * Definitions of the additional options that this course format uses for course 247 | * 248 | * Periods format uses the following options: 249 | * - coursedisplay 250 | * - numsections 251 | * - hiddensections 252 | * 253 | * @param bool $foreditform 254 | * @return array of options 255 | */ 256 | public function course_format_options($foreditform = false) { 257 | global $CFG; 258 | static $courseformatoptions = false; 259 | if ($courseformatoptions === false) { 260 | $courseconfig = get_config('moodlecourse'); 261 | $courseformatoptions = array( 262 | 'automaticenddate' => array( 263 | 'default' => 1, 264 | 'type' => PARAM_BOOL, 265 | ), 266 | 'periodduration' => array( 267 | 'default' => '1 week', // TODO this does not work. 268 | 'type' => PARAM_NOTAGS 269 | ), 270 | 'hiddensections' => array( 271 | 'default' => $courseconfig->hiddensections, 272 | 'type' => PARAM_INT, 273 | ), 274 | 'coursedisplay' => array( 275 | 'default' => $courseconfig->coursedisplay, 276 | 'type' => PARAM_INT, 277 | ), 278 | 'showfutureperiods' => array( 279 | 'default' => 0, 280 | 'type' => PARAM_INT 281 | ), 282 | 'futuresneakpeek' => array( 283 | 'default' => 0, 284 | 'type' => PARAM_INT 285 | ), 286 | 'showpastperiods' => array( 287 | 'default' => 0, 288 | 'type' => PARAM_INT 289 | ), 290 | 'showpastcompleted' => array( 291 | 'default' => 0, 292 | 'type' => PARAM_INT 293 | ), 294 | 'datesformat' => array( 295 | 'default' => 'strftimedateshort', 296 | 'type' => PARAM_ALPHANUMEXT 297 | ), 298 | 'datesformatcustom' => array( 299 | 'default' => '', 300 | 'type' => PARAM_NOTAGS 301 | ), 302 | ); 303 | } 304 | if ($foreditform && !isset($courseformatoptions['coursedisplay']['label'])) { 305 | 306 | require_once("$CFG->dirroot/course/format/periods/periodduration.php"); 307 | 308 | $datesformatlabels = array('strftimedateshort', 'strftimedatefullshort', 309 | 'strftimedate', 'strftimedatetime', 'strftimedatetimeshort', 310 | 'strftimedaydate', 'strftimedaydatetime', 'strftimedayshort', 311 | 'strftimedaytime', 'strftimemonthyear', 'strftimerecent', 312 | 'strftimerecentfull', 'strftimetime'); 313 | $datesformatoptions = array(); 314 | foreach ($datesformatlabels as $label) { 315 | $datesformatoptions[$label] = $label.' ('.get_string($label, 'langconfig').') - '. 316 | userdate(time(), get_string($label, 'langconfig')); 317 | } 318 | $datesformatoptions['custom'] = get_string('customdatesformat', 'format_periods'); 319 | $courseformatoptionsedit = array( 320 | 'automaticenddate' => array( 321 | 'label' => new lang_string('automaticenddate', 'format_periods'), 322 | 'help' => 'automaticenddate', 323 | 'help_component' => 'format_periods', 324 | 'element_type' => 'advcheckbox', 325 | ), 326 | 'periodduration' => array( 327 | 'label' => new lang_string('perioddurationdefault', 'format_periods'), 328 | 'help' => 'perioddurationdefault', 329 | 'help_component' => 'format_periods', 330 | 'element_type' => 'periodduration', 331 | 'element_attributes' => array(array('default' => '1 week')), 332 | ), 333 | 'hiddensections' => array( 334 | 'label' => new lang_string('hiddensections'), 335 | 'help' => 'hiddensections', 336 | 'help_component' => 'moodle', 337 | 'element_type' => 'select', 338 | 'element_attributes' => array( 339 | array( 340 | 0 => new lang_string('hiddensectionscollapsed'), 341 | 1 => new lang_string('hiddensectionsinvisible') 342 | ) 343 | ), 344 | ), 345 | 'coursedisplay' => array( 346 | 'label' => new lang_string('showperiods', 'format_periods'), 347 | 'element_type' => 'select', 348 | 'element_attributes' => array( 349 | array( 350 | FORMAT_PERIODS_EXPANDED => get_string('showexpanded', 'format_periods'), 351 | FORMAT_PERIODS_COLLAPSED => get_string('showcollapsed', 'format_periods'), 352 | ) 353 | ), 354 | 'help' => 'showperiods', 355 | 'help_component' => 'format_periods', 356 | ), 357 | 'showfutureperiods' => array( 358 | 'label' => new lang_string('showfutureperiods', 'format_periods'), 359 | 'help' => 'showfutureperiods', 360 | 'help_component' => 'format_periods', 361 | 'element_type' => 'select', 362 | 'element_attributes' => array( 363 | array( 364 | FORMAT_PERIODS_AS_ABOVE => get_string('sameascurrent', 'format_periods'), 365 | FORMAT_PERIODS_COLLAPSED => get_string('showcollapsed', 'format_periods'), 366 | FORMAT_PERIODS_NOTAVAILABLE => get_string('shownotavailable', 'format_periods'), 367 | FORMAT_PERIODS_HIDDEN => get_string('hidecompletely', 'format_periods'), 368 | ) 369 | ), 370 | ), 371 | 'futuresneakpeek' => array( 372 | 'label' => new lang_string('futuresneakpeek', 'format_periods'), 373 | 'help' => 'futuresneakpeek', 374 | 'help_component' => 'format_periods', 375 | 'element_type' => 'duration', 376 | 'element_attributes' => array( 377 | array('defaultunit' => 86400, 'optional' => false) 378 | ) 379 | ), 380 | 'showpastperiods' => array( 381 | 'label' => new lang_string('showpastperiods', 'format_periods'), 382 | 'help' => 'showpastperiods', 383 | 'help_component' => 'format_periods', 384 | 'element_type' => 'select', 385 | 'element_attributes' => array( 386 | array( 387 | FORMAT_PERIODS_AS_ABOVE => get_string('sameascurrent', 'format_periods'), 388 | FORMAT_PERIODS_COLLAPSED => get_string('showcollapsed', 'format_periods'), 389 | FORMAT_PERIODS_NOTDISPLAYED => get_string('hidefromcourseview', 'format_periods'), 390 | FORMAT_PERIODS_HIDDEN => get_string('hidecompletely', 'format_periods'), 391 | ) 392 | ), 393 | ), 394 | 'showpastcompleted' => array( 395 | 'label' => new lang_string('showpastcompleted', 'format_periods'), 396 | 'help' => 'showpastcompleted', 397 | 'help_component' => 'format_periods', 398 | 'element_type' => 'select', 399 | 'element_attributes' => array( 400 | array( 401 | FORMAT_PERIODS_AS_ABOVE => get_string('sameaspast', 'format_periods'), 402 | FORMAT_PERIODS_COLLAPSED => get_string('showcollapsed', 'format_periods'), 403 | FORMAT_PERIODS_NOTDISPLAYED => get_string('hidefromcourseview', 'format_periods'), 404 | FORMAT_PERIODS_HIDDEN => get_string('hidecompletely', 'format_periods'), 405 | ) 406 | ), 407 | ), 408 | 'datesformat' => array( 409 | 'label' => new lang_string('datesformat', 'format_periods'), 410 | 'help' => 'datesformat', 411 | 'help_component' => 'format_periods', 412 | 'element_type' => 'select', 413 | 'element_attributes' => array($datesformatoptions), 414 | ), 415 | 'datesformatcustom' => array( 416 | 'label' => new lang_string('datesformatcustom', 'format_periods'), 417 | 'help' => 'datesformatcustom', 418 | 'help_component' => 'format_periods', 419 | 'element_type' => 'text', 420 | ), 421 | ); 422 | $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit); 423 | } 424 | return $courseformatoptions; 425 | } 426 | 427 | /** 428 | * Definitions of the additional options that this course format uses for section 429 | * 430 | * See {@link format_base::course_format_options()} for return array definition. 431 | * 432 | * Additionally section format options may have property 'cache' set to true 433 | * if this option needs to be cached in {@link get_fast_modinfo()}. The 'cache' property 434 | * is recommended to be set only for fields used in {@link format_base::get_section_name()}, 435 | * {@link format_base::extend_course_navigation()} and {@link format_base::get_view_url()} 436 | * 437 | * For better performance cached options are recommended to have 'cachedefault' property 438 | * Unlike 'default', 'cachedefault' should be static and not access get_config(). 439 | * 440 | * Regardless of value of 'cache' all options are accessed in the code as 441 | * $sectioninfo->OPTIONNAME 442 | * where $sectioninfo is instance of section_info, returned by 443 | * get_fast_modinfo($course)->get_section_info($sectionnum) 444 | * or get_fast_modinfo($course)->get_section_info_all() 445 | * 446 | * All format options for particular section are returned by calling: 447 | * $this->get_format_options($section); 448 | * 449 | * @param bool $foreditform 450 | * @return array 451 | */ 452 | public function section_format_options($foreditform = false) { 453 | global $CFG; 454 | static $courseformatoptions = false; 455 | 456 | if ($courseformatoptions === false) { 457 | $courseformatoptions = array( 458 | 'periodduration' => array( 459 | 'type' => PARAM_NOTAGS 460 | ), 461 | ); 462 | } 463 | if ($foreditform && !isset($courseformatoptions['periodduration']['label'])) { 464 | 465 | require_once("$CFG->dirroot/course/format/periods/periodduration.php"); 466 | 467 | $courseformatoptionsedit = array( 468 | 'periodduration' => array( 469 | 'label' => new lang_string('perioddurationoverride', 'format_periods'), 470 | 'help' => 'perioddurationoverride', 471 | 'help_component' => 'format_periods', 472 | 'element_type' => 'periodduration', 473 | 'element_attributes' => array( 474 | array('optional' => true, 'default' => null) 475 | ) 476 | ), 477 | ); 478 | $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit); 479 | } 480 | return $courseformatoptions; 481 | } 482 | 483 | /** 484 | * Adds format options elements to the course/section edit form. 485 | * 486 | * This function is called from {@link course_edit_form::definition_after_data()}. 487 | * 488 | * @param MoodleQuickForm $mform form the elements are added to. 489 | * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form. 490 | * @return array array of references to the added form elements. 491 | */ 492 | public function create_edit_form_elements(&$mform, $forsection = false) { 493 | global $COURSE; 494 | $elements = parent::create_edit_form_elements($mform, $forsection); 495 | 496 | if (!$forsection && (empty($COURSE->id) || $COURSE->id == SITEID)) { 497 | // Add "numsections" element to the create course form - it will force new course to be prepopulated 498 | // with empty sections. 499 | // The "Number of sections" option is no longer available when editing course, instead teachers should 500 | // delete and add sections when needed. 501 | $courseconfig = get_config('moodlecourse'); 502 | $max = (int)$courseconfig->maxsections; 503 | $element = $mform->addElement('select', 'numsections', get_string('numberperiods', 'format_periods'), range(0, $max ?: 52)); 504 | $mform->setType('numsections', PARAM_INT); 505 | if (is_null($mform->getElementValue('numsections'))) { 506 | $mform->setDefault('numsections', $courseconfig->numsections); 507 | } 508 | array_unshift($elements, $element); 509 | } 510 | 511 | // Re-order things. 512 | if (!$forsection) { 513 | $mform->insertElementBefore($mform->removeElement('automaticenddate', false), 'idnumber'); 514 | $mform->disabledIf('enddate', 'automaticenddate', 'checked'); 515 | foreach ($elements as $key => $element) { 516 | if ($element->getName() == 'automaticenddate') { 517 | unset($elements[$key]); 518 | } 519 | } 520 | $elements = array_values($elements); 521 | } 522 | 523 | return $elements; 524 | } 525 | 526 | /** 527 | * Updates format options for a course 528 | * 529 | * In case if course format was changed to 'periods', we try to copy options 530 | * 'coursedisplay', 'numsections' and 'hiddensections' from the previous format. 531 | * If previous course format did not have 'numsections' option, we populate it with the 532 | * current number of sections 533 | * 534 | * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data 535 | * @param stdClass $oldcourse if this function is called from {@link update_course()} 536 | * this object contains information about the course before update 537 | * @return bool whether there were any changes to the options values 538 | */ 539 | public function update_course_format_options($data, $oldcourse = null) { 540 | global $DB; 541 | if ($oldcourse !== null) { 542 | $data = (array)$data; 543 | $oldcourse = (array)$oldcourse; 544 | $options = $this->course_format_options(); 545 | foreach ($options as $key => $unused) { 546 | if (!array_key_exists($key, $data)) { 547 | if (array_key_exists($key, $oldcourse)) { 548 | $data[$key] = $oldcourse[$key]; 549 | } else if ($key === 'numsections') { 550 | // If previous format does not have the field 'numsections' 551 | // and $data['numsections'] is not set, 552 | // we fill it with the maximum section number from the DB. 553 | $maxsection = $DB->get_field_sql('SELECT max(section) from {course_sections} 554 | WHERE course = ?', array($this->courseid)); 555 | if ($maxsection) { 556 | // If there are no sections, or just default 0-section, 'numsections' will be set to default. 557 | $data['numsections'] = $maxsection; 558 | } 559 | } 560 | } 561 | } 562 | } 563 | return $this->update_format_options($data); 564 | } 565 | 566 | /** 567 | * Return the start and end date of the passed section 568 | * 569 | * @param int|stdClass|section_info $section section to get the dates for 570 | * @return stdClass property start for startdate, property end for enddate 571 | */ 572 | public function get_section_dates($section, $startdate = null, $sections = null) { 573 | if ($startdate === null) { 574 | $startdate = $this->courseid ? $this->get_course()->startdate : 0; 575 | } 576 | $periodduration = '1 week'; 577 | if ($this->courseid) { 578 | $periodduration = $this->get_course()->periodduration; 579 | } 580 | if (is_object($section)) { 581 | $sectionnum = $section->section; 582 | } else { 583 | $sectionnum = $section; 584 | } 585 | 586 | $dates = new stdClass(); 587 | $dates->end = $dates->start = $startdate; 588 | 589 | $sections = ($sections !== null) ? $sections : $this->get_sections(); 590 | foreach ($sections as $snum => $sectioninfo) { 591 | if (!$snum) { 592 | continue; 593 | } else if ($snum <= $sectionnum) { 594 | $duration = $sectioninfo->periodduration ? $sectioninfo->periodduration : $periodduration; 595 | if (is_int($duration)) { 596 | $dt = $dates->start + $duration; 597 | } else { 598 | $dt = strtotime($duration, $dates->start); 599 | } 600 | if ($snum == $sectionnum) { 601 | $dates->end = $dt; 602 | } else { 603 | $dates->start = $dt; 604 | } 605 | } else { 606 | break; 607 | } 608 | } 609 | return $dates; 610 | } 611 | 612 | /** 613 | * Returns true if the specified week is current 614 | * 615 | * @param int|stdClass|section_info $section 616 | * @return bool 617 | */ 618 | public function is_section_current($section) { 619 | if (is_object($section)) { 620 | $sectionnum = $section->section; 621 | } else { 622 | $sectionnum = $section; 623 | } 624 | if ($sectionnum < 1) { 625 | return false; 626 | } 627 | $timenow = time(); 628 | $dates = $this->get_section_dates($section); 629 | return (($timenow >= $dates->start) && ($timenow < $dates->end)); 630 | } 631 | 632 | /** 633 | * Returns the display mode actually used by a particular section 634 | * 635 | * @param int|stdClass|section_info $section 636 | * @return int 637 | */ 638 | public function get_section_display_mode($section) { 639 | $course = $this->get_course(); 640 | $displaytype = $course->coursedisplay; 641 | 642 | if ($course->showfutureperiods == FORMAT_PERIODS_AS_ABOVE && 643 | $course->showpastperiods == FORMAT_PERIODS_AS_ABOVE && 644 | $course->showpastcompleted == FORMAT_PERIODS_AS_ABOVE) { 645 | // Shortcut, nothing else to do. 646 | return $displaytype; 647 | } 648 | 649 | $dates = $this->get_section_dates($section); 650 | $timenow = time(); 651 | if ($dates->start > $timenow + $course->futuresneakpeek) { 652 | // This is a future section. 653 | if ($course->showfutureperiods != FORMAT_PERIODS_AS_ABOVE) { 654 | $displaytype = $course->showfutureperiods; 655 | } 656 | } else if ($dates->end < $timenow) { 657 | // This is a past section. 658 | if ($course->showpastperiods != FORMAT_PERIODS_AS_ABOVE) { 659 | $displaytype = $course->showpastperiods; 660 | } 661 | if ($course->showpastcompleted != FORMAT_PERIODS_AS_ABOVE) { 662 | if ($this->is_section_completed($section)) { 663 | $displaytype = $course->showpastcompleted; 664 | } 665 | } 666 | } 667 | return $displaytype; 668 | } 669 | 670 | /** 671 | * Allows to specify for modinfo that section is not available even when it is visible and conditionally available. 672 | * 673 | * Note: affected user can be retrieved as: $section->modinfo->userid 674 | * 675 | * Course format plugins can override the method to change the properties $available and $availableinfo that were 676 | * calculated by conditional availability. 677 | * To make section unavailable set: 678 | * $available = false; 679 | * To make unavailable section completely hidden set: 680 | * $availableinfo = ''; 681 | * To make unavailable section visible with availability message set: 682 | * $availableinfo = get_string('sectionhidden', 'format_xxx'); 683 | * 684 | * @param section_info $section 685 | * @param bool $available the 'available' propery of the section_info as it was evaluated by conditional availability. 686 | * Can be changed by the method but 'false' can not be overridden by 'true'. 687 | * @param string $availableinfo the 'availableinfo' propery of the section_info as it was evaluated by conditional availability. 688 | * Can be changed by the method 689 | */ 690 | public function section_get_available_hook(section_info $section, &$available, &$availableinfo) { 691 | if (!$available || !$section->section) { 692 | return; 693 | } 694 | $displaytype = $this->get_section_display_mode($section); 695 | if ($displaytype == FORMAT_PERIODS_HIDDEN) { 696 | $available = false; 697 | $availableinfo = ''; 698 | } else if ($displaytype == FORMAT_PERIODS_NOTAVAILABLE) { 699 | $available = false; 700 | $availableinfo = get_string('notavailable', 'format_periods'); 701 | } 702 | } 703 | 704 | /** @var completion_info cached value of course completion info */ 705 | protected $completioninfo = null; 706 | 707 | /** 708 | * Evaluates if the section is completed. 709 | * 710 | * If the section was not completed at the start of the session but became 711 | * completed, this function will still return false. 712 | * 713 | * @param int|stdClass|section_info $section 714 | * @return bool 715 | */ 716 | public function is_section_completed($section) { 717 | if (is_object($section)) { 718 | $sectionnum = $section->section; 719 | } else { 720 | $sectionnum = $section; 721 | } 722 | global $SESSION; 723 | if (!empty($SESSION->format_periods[$this->courseid][$sectionnum])) { 724 | // This section was not completed at the beginning of the session, 725 | // consider it to be still not completed. 726 | return false; 727 | } 728 | if ($this->completioninfo === null) { 729 | $this->completioninfo = new completion_info($this->get_course()); 730 | } 731 | $modinfo = get_fast_modinfo($this->get_course()); 732 | if (!empty($modinfo->sections[$sectionnum])) { 733 | foreach ($modinfo->sections[$sectionnum] as $cmid) { 734 | $cm = $modinfo->cms[$cmid]; 735 | 736 | $completion = $this->completioninfo->is_enabled($cm); 737 | if ($completion != COMPLETION_TRACKING_NONE) { 738 | $completiondata = $this->completioninfo->get_data($cm, true); 739 | if ($completiondata->completionstate == COMPLETION_COMPLETE || 740 | $completiondata->completionstate == COMPLETION_COMPLETE_PASS) { 741 | // Section completed. 742 | continue; 743 | } 744 | } 745 | // This section is not completed. Remember this in the session so we 746 | // don't hide this section even if user completes everything. 747 | if (empty($SESSION->format_periods)) { 748 | $SESSION->format_periods = array(); 749 | } 750 | if (empty($SESSION->format_periods[$this->courseid])) { 751 | $SESSION->format_periods[$this->courseid] = array(); 752 | } 753 | $SESSION->format_periods[$this->courseid][$sectionnum] = 1; 754 | return false; 755 | } 756 | } 757 | return true; 758 | } 759 | 760 | /** 761 | * Whether this format allows to delete sections 762 | * 763 | * Do not call this function directly, instead use {@link course_can_delete_section()} 764 | * 765 | * @param int|stdClass|section_info $section 766 | * @return bool 767 | */ 768 | public function can_delete_section($section) { 769 | return true; 770 | } 771 | 772 | /** 773 | * Prepares the templateable object to display section name 774 | * 775 | * @param \section_info|\stdClass $section 776 | * @param bool $linkifneeded 777 | * @param bool $editable 778 | * @param null|lang_string|string $edithint 779 | * @param null|lang_string|string $editlabel 780 | * @return \core\output\inplace_editable 781 | */ 782 | public function inplace_editable_render_section_name($section, $linkifneeded = true, 783 | $editable = null, $edithint = null, $editlabel = null) { 784 | if (empty($edithint)) { 785 | $edithint = new lang_string('editsectionname', 'format_periods'); 786 | } 787 | if (empty($editlabel)) { 788 | $title = get_section_name($section->course, $section); 789 | $editlabel = new lang_string('newsectionname', 'format_periods', $title); 790 | } 791 | return parent::inplace_editable_render_section_name($section, $linkifneeded, $editable, $edithint, $editlabel); 792 | } 793 | 794 | /** 795 | * Returns whether this course format allows the activity to 796 | * have "triple visibility state" - visible always, hidden on course page but available, hidden. 797 | * 798 | * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module) 799 | * @param stdClass|section_info $section section where this module is located or will be added to 800 | * @return bool 801 | */ 802 | public function allow_stealth_module_visibility($cm, $section) { 803 | // Allow the third visibility state inside visible sections or in section 0. 804 | return !$section->section || $section->visible; 805 | } 806 | 807 | /** 808 | * Returns the default end date for periods course format. 809 | * 810 | * @param moodleform $mform 811 | * @param array $fieldnames The form - field names mapping. 812 | * @return int 813 | */ 814 | public function get_default_course_enddate($mform, $fieldnames = array()) { 815 | 816 | if (empty($fieldnames['startdate'])) { 817 | $fieldnames['startdate'] = 'startdate'; 818 | } 819 | 820 | if (empty($fieldnames['numsections'])) { 821 | $fieldnames['numsections'] = 'numsections'; 822 | } 823 | 824 | $startdate = $this->get_form_start_date($mform, $fieldnames); 825 | if ($mform->elementExists($fieldnames['numsections'])) { 826 | $numsections = $mform->getElementValue($fieldnames['numsections']); 827 | $numsections = $mform->getElement($fieldnames['numsections'])->exportValue($numsections); 828 | } else if ($this->get_courseid()) { 829 | // For existing courses get the number of sections. 830 | $numsections = $this->get_last_section_number(); 831 | } else { 832 | // Fallback to the default value for new courses. 833 | $numsections = get_config('moodlecourse', $fieldnames['numsections']); 834 | } 835 | 836 | // Final week's last day. 837 | $dates = $this->get_section_dates(intval($numsections), $startdate); 838 | return $dates->end; 839 | } 840 | 841 | /** 842 | * Updates the end date for a course in weeks format if option automaticenddate is set. 843 | * 844 | * This method is called from event observers and it can not use any modinfo or format caches because 845 | * events are triggered before the caches are reset. 846 | * 847 | * @param int $courseid 848 | */ 849 | public static function update_end_date($courseid) { 850 | global $DB, $COURSE; 851 | 852 | // Use one DB query to retrieve necessary fields in course, value for automaticenddate and number of the last 853 | // section. This query will also validate that the course is indeed in 'periods' format. 854 | $sql = "SELECT c.id, c.format, c.startdate, c.enddate, fo.value AS automaticenddate, d.value as periodduration 855 | FROM {course} c 856 | LEFT JOIN {course_format_options} fo 857 | ON fo.courseid = c.id 858 | AND fo.format = c.format 859 | AND fo.name = 'automaticenddate' 860 | AND fo.sectionid = 0 861 | LEFT JOIN {course_format_options} d 862 | ON d.courseid = c.id 863 | AND d.format = c.format 864 | AND d.name = 'periodduration' 865 | AND d.sectionid = 0 866 | WHERE c.format = :format 867 | AND c.id = :courseid"; 868 | $course = $DB->get_record_sql($sql, ['format' => 'periods', 'courseid' => $courseid]); 869 | 870 | if (!$course) { 871 | // Looks like it is a course in a different format, nothing to do here. 872 | return; 873 | } 874 | 875 | // Create an instance of this class and mock the course object. 876 | $format = new format_periods('periods', $courseid); 877 | $format->course = $course; 878 | 879 | // If automaticenddate is not specified take the default value. 880 | if (!isset($course->automaticenddate)) { 881 | $defaults = $format->course_format_options(); 882 | $course->automaticenddate = $defaults['automaticenddate']; 883 | } 884 | // Check that the course format for setting an automatic date is set. 885 | if (empty($course->automaticenddate)) { 886 | return; 887 | } 888 | 889 | if (!isset($course->periodduration)) { 890 | $defaults = isset($defaults) ? $defaults : $format->course_format_options(); 891 | $course->periodduration = $defaults['periodduration']; 892 | } 893 | 894 | $sections = $DB->get_records_sql("SELECT s.section, s.id, d.value as periodduration 895 | FROM {course_sections} s 896 | LEFT JOIN {course_format_options} d 897 | ON d.courseid = s.course 898 | AND d.format = 'periods' 899 | AND d.name = 'periodduration' 900 | AND d.sectionid = s.id 901 | WHERE s.course = :courseid AND s.section > 0 902 | ORDER BY s.section 903 | ", ['courseid' => $courseid]); 904 | 905 | // Get the final period's last day. 906 | if (!$sections) { 907 | $enddate = $course->startdate; 908 | } else { 909 | $dates = $format->get_section_dates(max(array_keys($sections)), null, $sections); 910 | $enddate = $dates->end; 911 | } 912 | 913 | // Set the course end date. 914 | if ($course->enddate != $enddate) { 915 | $DB->set_field('course', 'enddate', $enddate, array('id' => $course->id)); 916 | if (isset($COURSE->id) && $COURSE->id == $courseid) { 917 | $COURSE->enddate = $enddate; 918 | } 919 | } 920 | } 921 | 922 | public function supports_news() { 923 | return true; 924 | } 925 | } 926 | 927 | /** 928 | * Implements callback inplace_editable() allowing to edit values in-place 929 | * 930 | * @param string $itemtype 931 | * @param int $itemid 932 | * @param mixed $newvalue 933 | * @return \core\output\inplace_editable 934 | */ 935 | function format_periods_inplace_editable($itemtype, $itemid, $newvalue) { 936 | global $DB, $CFG; 937 | require_once($CFG->dirroot . '/course/lib.php'); 938 | if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') { 939 | $section = $DB->get_record_sql( 940 | 'SELECT s.* FROM {course_sections} s JOIN {course} c ON s.course = c.id WHERE s.id = ? AND c.format = ?', 941 | array($itemid, 'periods'), MUST_EXIST); 942 | return course_get_format($section->course)->inplace_editable_update_section_name($section, $itemtype, $newvalue); 943 | } 944 | } 945 | --------------------------------------------------------------------------------