├── 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 | *
12 | * - ...
13 | * - ...
14 | * ...
15 | *
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 |
--------------------------------------------------------------------------------