->set_type(column::TYPE_TIMESTAMP)
->add_field("{$tablealias}.timestart")
->set_is_sortable(true)
- ->add_callback([format::class, 'userdate']);
+ ->add_callback([format::class, 'userdate'], get_string('strftimedatetimeshortaccurate', 'core_langconfig'));
// Duration column.
$columns[] = (new column(
$PAGE->set_context($syscontext);
require_admin();
+require_sesskey();
$return = new moodle_url('/admin/settings.php', ['section' => 'manageqbanks']);
$parentname = 'competencies';
// Settings page.
+ $iscompetencyenabled = get_config('core_competency', 'enabled');
$settings = new admin_settingpage('competencysettings', new lang_string('competenciessettings', 'core_competency'),
- 'moodle/site:config', false);
- $ADMIN->add($parentname, $settings);
+ 'moodle/site:config', !$iscompetencyenabled);
+ if ($iscompetencyenabled) {
+ $ADMIN->add($parentname, $settings);
+ }
// Load the full tree of settings.
if ($ADMIN->fulltree) {
- $setting = new admin_setting_configcheckbox('core_competency/enabled',
- new lang_string('enablecompetencies', 'core_competency'),
- new lang_string('enablecompetencies_desc', 'core_competency'), 1);
- $settings->add($setting);
-
$setting = new admin_setting_configcheckbox('core_competency/pushcourseratingstouserplans',
new lang_string('pushcourseratingstouserplans', 'core_competency'),
new lang_string('pushcourseratingstouserplans_desc', 'core_competency'), 1);
defined('MOODLE_INTERNAL') || die();
if ($hassiteconfig) {
- $temp = new admin_settingpage('messages', new lang_string('messagingssettings', 'admin'));
- $temp->add(new admin_setting_configcheckbox('messaging',
- new lang_string('messaging', 'admin'),
- new lang_string('configmessaging', 'admin'),
- 1));
+ $temp = new admin_settingpage('messages',
+ new lang_string('messagingssettings', 'admin'),
+ 'moodle/site:config',
+ empty($CFG->messaging)
+ );
+
$temp->add(new admin_setting_configcheckbox('messagingallusers',
new lang_string('messagingallusers', 'admin'),
new lang_string('configmessagingallusers', 'admin'),
$optionalsubsystems->add(new admin_setting_configcheckbox('enableanalytics', new lang_string('enableanalytics', 'admin'),
new lang_string('configenableanalytics', 'admin'), 1, 1, 0));
+ $optionalsubsystems->add(new admin_setting_configcheckbox('core_competency/enabled',
+ new lang_string('enablecompetencies', 'core_competency'),
+ new lang_string('enablecompetencies_desc', 'core_competency'),
+ 1)
+ );
+
+ $optionalsubsystems->add(new admin_setting_configcheckbox('messaging',
+ new lang_string('messaging', 'admin'),
+ new lang_string('configmessaging', 'admin'),
+ 1)
+ );
+
$fullunicodesupport = true;
if ($DB->get_dbfamily() == 'mysql') {
$collation = $DB->get_dbcollation();
*
* All CLI utilities uses $CFG->behat_dataroot and $CFG->prefix_dataroot as
* $CFG->dataroot and $CFG->prefix
+ * Same applies for $CFG->behat_dbname, $CFG->behat_dbuser, $CFG->behat_dbpass
+ * and $CFG->behat_dbhost. But if any of those is not defined $CFG->dbname,
+ * $CFG->dbuser, $CFG->dbpass and/or $CFG->dbhost will be used.
*
* @package tool_behat
* @copyright 2012 David Monllaó
*
* All CLI utilities uses $CFG->behat_dataroot and $CFG->prefix_dataroot as
* $CFG->dataroot and $CFG->prefix
+ * Same applies for $CFG->behat_dbname, $CFG->behat_dbuser, $CFG->behat_dbpass
+ * and $CFG->behat_dbhost. But if any of those is not defined $CFG->dbname,
+ * $CFG->dbuser, $CFG->dbpass and/or $CFG->dbhost will be used.
*
* @package tool_behat
* @copyright 2012 David Monllaó
$string['errorcomposer'] = 'Composer dependencies are not installed.';
$string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
$string['errorsetconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot need to be set in config.php.';
-$string['erroruniqueconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot values need to be different than $CFG->dataroot, $CFG->prefix, $CFG->wwwroot, $CFG->phpunit_dataroot and $CFG->phpunit_prefix values.';
+$string['erroruniqueconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot values need to be different than $CFG->dataroot, $CFG->prefix, $CFG->wwwroot, $CFG->phpunit_dataroot and $CFG->phpunit_prefix values.<br/>Or, if $CFG->behat_prefix is the same, $CFG->behat_dbname or $CFG->behat_dbhost need to be different from $CFG->phpunit_dbname and $CFG->phpunit_dbhost and from $CFG->dbname and $CFG->dbhost.';
$string['fieldvalueargument'] = 'Field value arguments';
$string['fieldvalueargument_help'] = 'This argument should be completed by a field value. There are many field types, including simple ones like checkboxes, selects or textareas, or complex ones like date selectors. See the dev documentation <a href="https://docs.moodle.org/dev/Acceptance_testing" target="_blank">Acceptance_testing</a> for details of expected field values.';
$string['giveninfo'] = 'Given. Processes to set up the environment';
And I should see "Course 2"
And I follow "Cat 2"
And I should see "No courses in this category"
- And I follow "Miscellaneous"
+ And I follow "Category 1"
And I should see "Course 3"
@javascript
$contentlengthlimit = 500;
$pagetext = '';
- foreach ($this->get_all_elements(null, 'text') as $element) {
- $text = $element->nodeValue;
+ // There will be only one, but array is returned anyway.
+ foreach ($this->get_all_elements('body') as $element) {
+ $text = $element->textContent;
if ($text != null) {
$pagetext = $pagetext . $text;
}
}
- $wordcount = str_word_count($pagetext);
+ $wordcount = count_words($pagetext);
if ($wordcount > $contentlengthlimit) {
$this->add_report(null, "<p id='wc'>Word Count: " . $wordcount . "</p>", false);
}
/** @var string Html fail */
private $htmlfail = <<<EOD
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
- <html lang="en">
- <head>
- <title>Content must not exceed a certain length</title>
- </head>
- <body>
- <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent accumsan, ante varius viverra aliquam, dolor risus
+ <p><span>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent accumsan, ante varius viverra aliquam, dolor risus
scelerisque massa, ut lacinia ipsum felis id est. Nullam convallis odio ante, in commodo elit fermentum sed. Vivamus ullamcorper
tincidunt sagittis. Sed et semper sapien. Quisque malesuada lacus nec libero cursus, aliquam malesuada neque ultricies. Cras sit
amet enim vel orci tristique porttitor a vitae urna. Suspendisse mi leo, hendrerit et eleifend a, mollis at ex. Maecenas eget
magna nec sem dignissim pharetra vel nec ex. Donec in porta lectus. Aenean porttitor euismod lectus, sodales eleifend ex egestas
- in. Donec sed metus sodales, lobortis velit quis, dictum arcu.
- Praesent mollis urna eget odio cursus, sit amet sollicitudin ante aliquam. Integer nec massa nec ipsum tincidunt laoreet in
- vitae metus. Integer massa lacus, elementum quis dui sed, eleifend fringilla turpis. In hac habitasse platea dictumst. Phasellus
+ in. Donec sed metus sodales, lobortis velit quis, dictum arcu.</span></p>
+ <p><span>Praesent mollis urna eget odio cursus, sit amet sollicitudin ante aliquam. Integer nec massa nec ipsum tincidunt
+ laoreet in vitae metus.
+ Integer massa lacus, elementum quis dui sed, eleifend fringilla turpis. In hac habitasse platea dictumst. Phasellus
efficitur quis felis non eleifend. Sed et mauris vel lorem ultrices porta. Mauris commodo condimentum felis, vel dictum ex
laoreet sit amet. Duis venenatis ut lacus non ultrices. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per
inceptos himenaeos. Nam nunc magna, semper feugiat feugiat a, pellentesque vel nulla.
blandit eget elit sit amet, suscipit mollis ligula. Suspendisse rutrum sem ex, eu commodo nisi aliquam sit amet. Fusce ut felis
justo. Sed a quam at lectus consectetur vulputate. Proin elementum dui nisi, in condimentum diam porttitor eget. Donec vehicula
condimentum velit vel semper. Mauris vehicula tortor lectus, quis convallis erat aliquet vel. In dictum nunc ac posuere porta.
- Sed vel leo aliquam, volutpat ligula ac, blandit diam. Donec nec ligula lacus.
- Mauris ac libero vel ex fringilla fringilla. Ut vehicula justo eu nunc imperdiet ultricies. Sed interdum ligula at nisi rhoncus
- auctor. Sed tempus tellus eget risus placerat, et viverra dolor gravida. Sed ultricies neque id ex tempor viverra. Ut imperdiet
+ Sed vel leo aliquam, volutpat ligula ac, blandit diam. Donec nec ligula lacus.</span></p>
+ <p><span>Mauris ac libero vel ex fringilla fringilla. Ut vehicula justo eu nunc imperdiet ultricies. Sed interdum ligula at nisi
+ rhoncus auctor.
+ Sed tempus tellus eget risus placerat, et viverra dolor gravida. Sed ultricies neque id ex tempor viverra. Ut imperdiet
pharetra magna sed tristique. Pellentesque blandit elit ac neque lacinia finibus. Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Donec vel auctor dolor. Morbi id elit mollis ante mattis semper eu ac lectus. Integer elit turpis, facilisis
vel metus eget, blandit tempus arcu. Pellentesque eget magna eu ex eleifend tincidunt. Curabitur sit amet congue nisi.
turpis. Aenean tincidunt tristique dui, pretium lacinia felis posuere vel. Donec massa ligula, luctus vitae enim nec, sagittis
hendrerit lorem. In consequat sodales metus vel porttitor. Aenean fringilla fringilla risus, vitae interdum turpis egestas quis.
Aenean volutpat arcu leo, ut dictum purus consectetur id. Cras enim ipsum, tincidunt vitae mi vel, varius convallis ex. Fusce
- pretium porttitor tempus.
- Morbi laoreet dapibus lectus ut efficitur. Donec at hendrerit nunc. Vivamus venenatis augue non nulla finibus vestibulum. Nam
+ pretium porttitor tempus.</span></p>
+ <p>Morbi laoreet dapibus lectus ut efficitur. Donec at hendrerit nunc. Vivamus venenatis augue non nulla finibus vestibulum. Nam
nunc magna, hendrerit a ipsum nec, pulvinar imperdiet augue. Fusce vel metus maximus, mattis magna at, egestas enim. Suspendisse
et nisl at enim mollis scelerisque. Duis ut ipsum vel turpis eleifend aliquet a a ante. Nam lacinia purus vulputate purus
tincidunt, aliquet sagittis nisi sagittis. Pellentesque efficitur massa non ex sodales pretium. Cras convallis vitae ex et
dignissim. Nunc suscipit bibendum aliquam. Maecenas interdum tellus varius, laoreet velit sed, ornare arcu. Nunc pulvinar
elementum sem eget scelerisque. Duis volutpat tellus ut risus finibus, nec molestie erat fermentum
</p>
- </body>
- </html>
+EOD;
+
+ /** @var string Multibyte html falure */
+ private $htmlfail2 = <<<EOD
+ <p><span>ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+ </span></p>
EOD;
/** @var string Html pass */
private $htmlpass = <<<EOD
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
- <html lang="en">
- <head>
- <title>Content must not exceed a certain length/title>
- </head>
- <body>
<p>Nice and short text</p>
- </body>
- </html>
EOD;
/**
$results = $this->get_checker_results($this->htmlfail);
$this->assertTrue($results[0]->message == '<p id=\'wc\'>Word Count: 578</p>');
+ $results = $this->get_checker_results($this->htmlfail2);
+ $this->assertTrue($results[0]->message == '<p id=\'wc\'>Word Count: 504</p>');
+
$results = $this->get_checker_results($this->htmlpass);
$this->assertEmpty($results);
}
| Roles: | Student |
And I click on "Get the overview" "button"
Then I should see "Permissions in System"
- And I should see "Permissions in Category: Miscellaneous"
+ And I should see "Permissions in Category: Category 1"
And I should see "Permissions in Course: Course 1"
And I should not see "Permissions in Course: Course 2"
| Roles: | Student |
And I click on "Get the overview" "button"
Then I should see "Permissions in System"
- And I should not see "Permissions in Category: Miscellaneous"
+ And I should not see "Permissions in Category: Category 1"
And I should not see "Permissions in Course: Course 1"
And I should not see "Permissions in Course: Course 2"
| Roles: | Student |
And I click on "Get the overview" "button"
Then I should see "Permissions in System"
- And I should see "Permissions in Category: Miscellaneous"
+ And I should see "Permissions in Category: Category 1"
And I should see "Permissions in Course: Course 1"
And I should not see "Permissions in Course: Course 2"
| Roles: | Student |
And I click on "Get the overview" "button"
Then I should see "Permissions in System"
- And I should see "Permissions in Category: Miscellaneous"
+ And I should see "Permissions in Category: Category 1"
And I should see "Permissions in Course: Course 1"
And I should not see "Permissions in Course: Course 2"
| Roles: | Student |
And I click on "Get the overview" "button"
Then I should see "Permissions in System"
- And I should not see "Permissions in Category: Miscellaneous"
+ And I should not see "Permissions in Category: Category 1"
And I should not see "Permissions in Course: Course 1"
And I should not see "Permissions in Course: Course 2"
| Roles: | |
And I click on "Get the overview" "button"
Then I should see "Permissions in System"
- And I should see "Permissions in Category: Miscellaneous"
+ And I should see "Permissions in Category: Category 1"
And I should see "Permissions in Course: Course 1"
And I should not see "Permissions in Course: Course 2"
| Roles: | |
And I click on "Get the overview" "button"
Then I should see "Permissions in System"
- And I should not see "Permissions in Category: Miscellaneous"
+ And I should not see "Permissions in Category: Category 1"
And I should not see "Permissions in Course: Course 1"
And I should not see "Permissions in Course: Course 2"
| Roles: | |
And I click on "Get the overview" "button"
Then I should see "Permissions in System"
- And I should see "Permissions in Category: Miscellaneous"
+ And I should see "Permissions in Category: Category 1"
And I should see "Permissions in Course: Course 1"
And I should see "Permissions in Course: Course 2"
And I set the field "Show differences only" to "1"
And I click on "Get the overview" "button"
Then I should see "Permissions in System"
- And I should see "Permissions in Category: Miscellaneous"
+ And I should see "Permissions in Category: Category 1"
And I should see "There are no differences to show between selected roles in this context"
And I should see "Permissions in Course: Course 1"
And I should not see "Permissions in Course: Course 2"
And I click on "Get the overview" "button"
Then I should see "Permissions in System"
And I should see "There are no differences to show between selected roles in this context"
- And I should not see "Permissions in Category: Miscellaneous"
+ And I should not see "Permissions in Category: Category 1"
And I should not see "Permissions in Course: Course 1"
And I should not see "Permissions in Course: Course 2"
And I set the field "Show differences only" to "1"
And I click on "Get the overview" "button"
Then I should see "Permissions in System"
- And I should see "Permissions in Category: Miscellaneous"
+ And I should see "Permissions in Category: Category 1"
And I should see "There are no differences to show between selected roles in this context"
And I should see "Permissions in Course: Course 1"
And I should not see "Permissions in Course: Course 2"
"courseimage": "https://placekitten.com/300/500",
"fullname": "Mathematics Year One",
"isfavourite": true,
- "coursecategory": "Miscellaneous",
+ "coursecategory": "Category 1",
"showcoursecategory": true,
"visible": true
}
"courseimage": "https://placekitten.com/300/500",
"fullname": "Mathematics Year One",
"isfavourite": true,
- "coursecategory": "Miscellaneous",
+ "coursecategory": "Category 1",
"showcoursecategory": true,
"visible": true
},
$string['copied'] = 'Copied!';
$string['copy'] = 'Copy';
$string['copytoclipboard'] = 'Copy to clipboard';
-$string['installer'] = '<h3>Please setup this component library</h3>
+$string['installer'] = '<h3>Component library setup</h3>
<p>Before you can see the content of the component library you will need to have shell access to your Moodle installation and be able to write to folder /admin/tool/componentlibrary and have npm installed on your Moodle server.</p>
<p>If you meet these requirements you can navigate to your Moodle root folder and run:</p>
<pre>$ npm install</pre>
<p>This will fetch all the required packages for building the component library docs.</p>
<p>Once they are installed you can run:</p>
<pre>$ grunt componentlibrary</pre>
- <p>For more info see the README.md file in this plugin</p>';
+ <p>For more info see the README.md file in this plugin.</p>';
$string['pluginname'] = 'UI Component library';
$string['privacy:metadata'] = 'The Component library plugin does not store any personal data.';
$string['showboth'] = 'Show with both';
*/
defined('MOODLE_INTERNAL') || die;
-// Manage competency frameworks page.
-$temp = new admin_externalpage(
- 'toollpimportcsv',
- get_string('pluginname', 'tool_lpimportcsv'),
- new moodle_url('/admin/tool/lpimportcsv/index.php'),
- 'moodle/competency:competencymanage'
-);
-$ADMIN->add('competencies', $temp);
-// Export competency framework page.
-$temp = new admin_externalpage(
- 'toollpexportcsv',
- get_string('exportnavlink', 'tool_lpimportcsv'),
- new moodle_url('/admin/tool/lpimportcsv/export.php'),
- 'moodle/competency:competencymanage'
-);
-$ADMIN->add('competencies', $temp);
+if (get_config('core_competency', 'enabled')) {
+ // Manage competency frameworks page.
+ $temp = new admin_externalpage(
+ 'toollpimportcsv',
+ get_string('pluginname', 'tool_lpimportcsv'),
+ new moodle_url('/admin/tool/lpimportcsv/index.php'),
+ 'moodle/competency:competencymanage'
+ );
+ $ADMIN->add('competencies', $temp);
+ // Export competency framework page.
+ $temp = new admin_externalpage(
+ 'toollpexportcsv',
+ get_string('exportnavlink', 'tool_lpimportcsv'),
+ new moodle_url('/admin/tool/lpimportcsv/export.php'),
+ 'moodle/competency:competencymanage'
+ );
+ $ADMIN->add('competencies', $temp);
+}
// No report settings.
$settings = null;
if ($hassiteconfig) {
- $ADMIN->add('root', new admin_category('mobileapp', new lang_string('mobileapp', 'tool_mobile')), 'development');
-
- $temp = new admin_settingpage('mobilesettings', new lang_string('mobilesettings', 'tool_mobile'), 'moodle/site:config', false);
-
// We should wait to the installation to finish since we depend on some configuration values that are set once
// the admin user profile is configured.
if (!during_initial_install()) {
$enablemobiledocurl = new moodle_url(get_docs_url('Enable_mobile_web_services'));
$enablemobiledoclink = html_writer::link($enablemobiledocurl, new lang_string('documentation'));
$default = is_https() ? 1 : 0;
- $temp->add(new admin_setting_enablemobileservice('enablemobilewebservice',
+ $optionalsubsystems = $ADMIN->locate('optionalsubsystems');
+ $optionalsubsystems->add(new admin_setting_enablemobileservice('enablemobilewebservice',
new lang_string('enablemobilewebservice', 'admin'),
new lang_string('configenablemobilewebservice', 'admin', $enablemobiledoclink), $default));
}
+ $ismobilewsdisabled = empty($CFG->enablemobilewebservice);
+ $ADMIN->add('root',
+ new admin_category('mobileapp', new lang_string('mobileapp', 'tool_mobile'), $ismobilewsdisabled),
+ 'development'
+ );
+
+ $temp = new admin_settingpage('mobilesettings',
+ new lang_string('mobilesettings', 'tool_mobile'),
+ 'moodle/site:config',
+ $ismobilewsdisabled
+ );
+
$temp->add(new admin_setting_configtext('tool_mobile/apppolicy', new lang_string('apppolicy', 'tool_mobile'),
new lang_string('apppolicy_help', 'tool_mobile'), '', PARAM_URL));
$featuresnotice = $OUTPUT->render($notify);
}
- $hideappsubscription = empty($CFG->enablemobilewebservice);
- $hideappsubscription = $hideappsubscription || (isset($CFG->disablemobileappsubscription) && !empty($CFG->disablemobileappsubscription));
+ $hideappsubscription = (isset($CFG->disablemobileappsubscription) && !empty($CFG->disablemobileappsubscription));
+ $hideappsubscription = $ismobilewsdisabled || $hideappsubscription;
$ADMIN->add(
'mobileapp',
'mobileauthentication',
new lang_string('mobileauthentication', 'tool_mobile'),
'moodle/site:config',
- empty($CFG->enablemobilewebservice)
+ $ismobilewsdisabled
);
$temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesauth', '', $featuresnotice));
'mobileappearance',
new lang_string('mobileappearance', 'tool_mobile'),
'moodle/site:config',
- empty($CFG->enablemobilewebservice)
+ $ismobilewsdisabled
);
if (!empty($featuresnotice)) {
'mobilefeatures',
new lang_string('mobilefeatures', 'tool_mobile'),
'moodle/site:config',
- empty($CFG->enablemobilewebservice)
+ $ismobilewsdisabled
);
if (!empty($featuresnotice)) {
defined('MOODLE_INTERNAL') || die();
if ($hassiteconfig) {
+ // Add an enable subsystem setting to the "Advanced features" settings page.
+ $optionalsubsystems = $ADMIN->locate('optionalsubsystems');
+ $optionalsubsystems->add(new admin_setting_configcheckbox('tool_moodlenet/enablemoodlenet',
+ new lang_string('enablemoodlenet', 'tool_moodlenet'),
+ new lang_string('enablemoodlenet_desc', 'tool_moodlenet'),
+ 0, 1, 0)
+ );
+
// Create a MoodleNet category.
- $ADMIN->add('root', new admin_category('moodlenet', get_string('pluginname', 'tool_moodlenet')));
- // Our settings page.
- $settings = new admin_settingpage('tool_moodlenet', get_string('moodlenetsettings', 'tool_moodlenet'));
- $ADMIN->add('moodlenet', $settings);
+ if (get_config('tool_moodlenet', 'enablemoodlenet')) {
+ $ADMIN->add('root', new admin_category('moodlenet', get_string('pluginname', 'tool_moodlenet')));
+ // Our settings page.
+ $settings = new admin_settingpage('tool_moodlenet', get_string('moodlenetsettings', 'tool_moodlenet'));
+ $ADMIN->add('moodlenet', $settings);
- $temp = new admin_setting_configcheckbox('tool_moodlenet/enablemoodlenet', get_string('enablemoodlenet', 'tool_moodlenet'),
- new lang_string('enablemoodlenet_desc', 'tool_moodlenet'), 0, 1, 0);
- $settings->add($temp);
+ $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenetname',
+ get_string('defaultmoodlenetname', 'tool_moodlenet'), new lang_string('defaultmoodlenetname_desc', 'tool_moodlenet'),
+ new lang_string('defaultmoodlenetnamevalue', 'tool_moodlenet'));
+ $settings->add($temp);
- $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenetname',
- get_string('defaultmoodlenetname', 'tool_moodlenet'), new lang_string('defaultmoodlenetname_desc', 'tool_moodlenet'),
- new lang_string('defaultmoodlenetnamevalue', 'tool_moodlenet'));
- $settings->add($temp);
+ $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenet', get_string('defaultmoodlenet', 'tool_moodlenet'),
+ new lang_string('defaultmoodlenet_desc', 'tool_moodlenet'), 'https://moodle.net');
+ $settings->add($temp);
- $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenet', get_string('defaultmoodlenet', 'tool_moodlenet'),
- new lang_string('defaultmoodlenet_desc', 'tool_moodlenet'), 'https://moodle.net');
- $settings->add($temp);
+ }
}
"viewurl": "https://moodlesite/course/view.php?id=2",
"courseimage": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
"fullname": "course 3",
- "coursecategory": "Miscellaneous",
+ "coursecategory": "Category 1",
"visible": true
}
]
$formatexpected = format_time($expectedfrequency);
$formatinterval = format_time($lastcroninterval);
- $details = format_time($delta);
+ // Inform user the time since last cron start.
+ $details = get_string('lastcronstart', 'tool_task', $formatdelta);
if ($delta > $expectedfrequency + MINSECS) {
$status = result::WARNING;
$string['faildelay'] = 'Fail delay';
$string['fromcomponent'] = 'From component: {$a}';
$string['hostname'] = 'Host name';
+$string['lastcronstart'] = 'Time since last cron run: {$a}';
$string['lastruntime'] = 'Last run';
$string['lastupdated'] = 'Last updated {$a}.';
$string['nextruntime'] = 'Next run';
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category'));
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category 1'));
- $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Miscellaneous'));
+ $this->assertCount(
+ 2,
+ \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT],
+ get_string('defaultcategoryname')
+ ));
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course 1'));
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course'));
}
$this->resetAfterTest(true);
$this->setAdminuser();
- $misc = $DB->get_record('course_categories', ['name' => 'Miscellaneous']);
+ $misc = $DB->get_record('course_categories', ['name' => get_string('defaultcategoryname')]);
$miscctx = \context_coursecat::instance($misc->id);
$category = $this->getDataGenerator()->create_category();
$authdb->Close();
if ($this->config->passtype === 'plaintext') {
- return ($fromdb == $extpassword);
+ return ($fromdb === $extpassword);
} else if ($this->config->passtype === 'md5') {
- return (strtolower($fromdb) == md5($extpassword));
+ return (strtolower($fromdb) === md5($extpassword));
} else if ($this->config->passtype === 'sha1') {
- return (strtolower($fromdb) == sha1($extpassword));
+ return (strtolower($fromdb) === sha1($extpassword));
} else if ($this->config->passtype === 'saltedcrypt') {
return password_verify($extpassword, $fromdb);
} else {
$DB->update_record('auth_db_users', $user3);
$this->assertTrue($auth->user_login('u3', 'heslo'));
+ // Test user created to see if the checking happens strictly.
+ $usermd5 = (object)['name' => 'usermd5', 'pass' => '0e462097431906509019562988736854'];
+ $usermd5->id = $DB->insert_record('auth_db_users', $usermd5);
+
+ // md5('240610708') === '0e462097431906509019562988736854'.
+ $this->assertTrue($auth->user_login('usermd5', '240610708'));
+ $this->assertFalse($auth->user_login('usermd5', 'QNKCDZO'));
+
set_config('passtype', 'sh1', 'auth_db');
$auth->config->passtype = 'sha1';
$user3->pass = sha1('heslo');
$DB->update_record('auth_db_users', $user3);
$this->assertTrue($auth->user_login('u3', 'heslo'));
+ // Test user created to see if the checking happens strictly.
+ $usersha1 = (object)['name' => 'usersha1', 'pass' => '0e66507019969427134894567494305185566735'];
+ $usersha1->id = $DB->insert_record('auth_db_users', $usersha1);
+
+ // sha1('aaroZmOk') === '0e66507019969427134894567494305185566735'.
+ $this->assertTrue($auth->user_login('usersha1', 'aaroZmOk'));
+ $this->assertFalse($auth->user_login('usersha1', 'aaK1STfY'));
+
set_config('passtype', 'saltedcrypt', 'auth_db');
$auth->config->passtype = 'saltedcrypt';
$user3->pass = password_hash('heslo', PASSWORD_BCRYPT);
empty($nuvalue) ? $nuvalue = array() : $nuvalue;
$ouvalue = core_text::convert($oldvalue, 'utf-8', $this->config->ldapencoding);
foreach ($ldapkeys as $ldapkey) {
- // Skip update if $ldapkey does not exist in LDAP.
- if (!isset($user_entry[$ldapkey][0])) {
- $success = false;
- error_log($this->errorlogtag.get_string('updateremfailfield', 'auth_ldap',
- array('ldapkey' => $ldapkey,
- 'key' => $key,
- 'ouvalue' => $ouvalue,
- 'nuvalue' => $nuvalue)));
- continue;
+ // If the field is empty in LDAP there are two options:
+ // 1. We get the LDAP field using ldap_first_attribute.
+ // 2. LDAP don't send the field using ldap_first_attribute.
+ // So, for option 1 we check the if the field is retrieve it.
+ // And get the original value of field in LDAP if the field.
+ // Otherwise, let value in blank and delegate the check in ldap_modify.
+ if (isset($user_entry[$ldapkey][0])) {
+ $ldapvalue = $user_entry[$ldapkey][0];
+ } else {
+ $ldapvalue = '';
}
- $ldapvalue = $user_entry[$ldapkey][0];
if (!$ambiguous) {
// Skip update if the values already match
if ($nuvalue !== $ldapvalue) {
$string['start_tls_key'] = 'Use TLS';
$string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}<br/>Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
$string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\'';
-$string['updateremfailfield'] = 'Failed to update LDAP with non-existent field (\'{$a->ldapkey}\'). Key ({$a->key}) - old Moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
$string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}';
$string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}';
$string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expiry time and/or grace logins. Error code: {$a->errno}; Error string: {$a->errstring}';
foreach ($sessions as $session) {
// Get user session from DB.
- if (session_decode(base64_decode($session->sessdata))) {
- if (isset($_SESSION['SESSION']) && isset($_SESSION['SESSION']->shibboleth_session_id)) {
- // If there is a match, kill the session.
- if ($_SESSION['SESSION']->shibboleth_session_id == trim($spsessionid)) {
- // Delete this user's sessions.
- \core\session\manager::kill_user_sessions($session->userid);
- }
+ $usersession = self::unserializesession(base64_decode($session->sessdata));
+ if (isset($usersession['SESSION']) && isset($usersession['SESSION']->shibboleth_session_id)) {
+ // If there is a match, kill the session.
+ if ($usersession['SESSION']->shibboleth_session_id == trim($spsessionid)) {
+ // Delete this user's sessions.
+ \core\session\manager::kill_user_sessions($session->userid);
}
}
}
<ID>1</ID>
<CATEGORY>
<ID>1</ID>
- <NAME>Miscellaneous</NAME>
+ <NAME>Category 1</NAME>
</CATEGORY>
<PASSWORD></PASSWORD>
<FULLNAME>[#course_name#]</FULLNAME>
<ID>2</ID>
<CATEGORY>
<ID>1</ID>
- <NAME>Miscellaneous</NAME>
+ <NAME>Category 1</NAME>
</CATEGORY>
<PASSWORD></PASSWORD>
<FULLNAME>Course Fullname 101</FULLNAME>
<ID>33</ID>
<CATEGORY>
<ID>1</ID>
- <NAME>Miscellaneous</NAME>
+ <NAME>Category 1</NAME>
</CATEGORY>
<PASSWORD></PASSWORD>
<FULLNAME>Moodle 2.0 Test Restore</FULLNAME>
* @throws base_setting_ui_exception when the label is not valid.
* @param string $label
*/
- public function set_label($label) {
- $label = (string)$label;
- if ($label === '' || $label !== clean_param($label, PARAM_TEXT)) {
+ public function set_label(string $label) :void {
+ $label = clean_param($label, PARAM_CLEANHTML);
+
+ if ($label === '') {
throw new base_setting_ui_exception('setting_invalid_ui_label');
}
+
$this->label = $label;
}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tests for base_setting_ui class.
+ *
+ * @package core_backup
+ * @copyright 2021 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot.'/backup/util/settings/tests/settings_test.php');
+
+/**
+ * Tests for base_setting_ui class.
+ *
+ * @copyright 2021 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class base_setting_ui_test extends advanced_testcase {
+ /**
+ * Tests set_label().
+ *
+ * @return void
+ */
+ public function test_set_label() {
+ $this->resetAfterTest();
+
+ $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN);
+ $bsui = new base_setting_ui($bs);
+
+ // Should keep original text string.
+ $bsui->set_label('Section name');
+ $this->assertEquals('Section name', $bsui->get_label());
+
+ // Should keep original HTML string.
+ $bsui->set_label('<b>Section name</b>');
+ $this->assertEquals('<b>Section name</b>', $bsui->get_label());
+
+ // Should be converted to text string.
+ $bsui->set_label(123);
+ $this->assertSame('123', $bsui->get_label());
+
+ // Should raise an exception when label is empty.
+ try {
+ $bsui->set_label('');
+ $this->assertTrue(false, 'base_setting_ui_exception');
+ } catch (Exception $exception) {
+ $this->assertTrue($exception instanceof base_setting_ui_exception);
+ $this->assertEquals($exception->errorcode, 'setting_invalid_ui_label');
+ }
+ }
+}
Background:
Given the following "categories" exist:
| name | category | idnumber |
- | Category 1 | 0 | CAT1 |
- | Category 2 | 0 | CAT2 |
- | Category 3 | CAT2 | CAT3 |
+ | Category A | 0 | CATA |
+ | Category B | 0 | CATB |
+ | Category C | CATB | CATC |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
- | Course 2 | C2 | CAT1 |
- | Course 3 | C3 | CAT2 |
- | Course 4 | C4 | CAT3 |
+ | Course 2 | C2 | CATA |
+ | Course 3 | C3 | CATB |
+ | Course 4 | C4 | CATC |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | First | teacher1@example.com |
And I am on site homepage
And I turn editing mode on
And I am on course index
- And I follow "Miscellaneous"
+ And I follow "Category 1"
And I add the "Courses" block
And I log out
When I log in as "teacher1"
And I am on course index
- And I follow "Miscellaneous"
+ And I follow "Category 1"
Then I should see "Course 1" in the "My courses" "block"
And I should see "Course 2" in the "My courses" "block"
And I should see "Course 3" in the "My courses" "block"
And I should not see "Course 4" in the "My courses" "block"
And I follow "All courses"
- And I should see "Miscellaneous"
+ And I should see "Category 1"
Scenario: Add the course list block on category page and navigate to another course
Given I log in as "admin"
And I am on site homepage
And I turn editing mode on
And I am on course index
- And I follow "Miscellaneous"
+ And I follow "Category 1"
And I add the "Courses" block
And I log out
When I log in as "teacher1"
And I am on course index
- And I follow "Miscellaneous"
+ And I follow "Category 1"
Then I should see "Course 1" in the "My courses" "block"
And I should see "Course 2" in the "My courses" "block"
And I should see "Course 3" in the "My courses" "block"
And I am on site homepage
And I turn editing mode on
And I am on course index
- And I follow "Miscellaneous"
+ And I follow "Category 1"
When I add the "Courses" block
- Then I should see "Miscellaneous" in the "Course categories" "block"
- And I should see "Category 1" in the "Course categories" "block"
- And I should see "Category 2" in the "Course categories" "block"
- And I should not see "Category 3" in the "Course categories" "block"
+ Then I should see "Category 1" in the "Course categories" "block"
+ And I should see "Category A" in the "Course categories" "block"
+ And I should see "Category B" in the "Course categories" "block"
+ And I should not see "Category C" in the "Course categories" "block"
And I should not see "Course 1" in the "Course categories" "block"
And I should not see "Course 2" in the "Course categories" "block"
And I follow "All courses"
- And I should see "Miscellaneous"
+ And I should see "Category 1"
Background:
Given the following "categories" exist:
| name | category | idnumber |
- | Category 1 | 0 | CAT1 |
- | Category 2 | 0 | CAT2 |
- | Category 3 | CAT2 | CAT3 |
+ | Category A | 0 | CATA |
+ | Category B | 0 | CATB |
+ | Category C | CATB | CATC |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
- | Course 2 | C2 | CAT1 |
- | Course 3 | C3 | CAT2 |
- | Course 4 | C4 | CAT3 |
+ | Course 2 | C2 | CATA |
+ | Course 3 | C3 | CATB |
+ | Course 4 | C4 | CATC |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | First | teacher1@example.com |
And I should see "Course 3" in the "My courses" "block"
And I should not see "Course 4" in the "My courses" "block"
And I follow "All courses"
- And I should see "Miscellaneous"
+ And I should see "Category 1"
Scenario: Add the course list block on course page and navigate to another course
Given I log in as "teacher1"
Given I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
When I add the "Courses" block
- Then I should see "Miscellaneous" in the "Course categories" "block"
- And I should see "Category 1" in the "Course categories" "block"
- And I should see "Category 2" in the "Course categories" "block"
- And I should not see "Category 3" in the "Course categories" "block"
+ Then I should see "Category 1" in the "Course categories" "block"
+ And I should see "Category A" in the "Course categories" "block"
+ And I should see "Category B" in the "Course categories" "block"
+ And I should not see "Category C" in the "Course categories" "block"
And I should not see "Course 1" in the "Course categories" "block"
And I should not see "Course 2" in the "Course categories" "block"
And I follow "All courses"
- And I should see "Miscellaneous"
+ And I should see "Category 1"
Scenario: View the course list block on course page with hide all courses link enabled
Given the following config values are set as admin:
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
When I add the "Courses" block
- Then I should not see "Miscellaneous" in the "My courses" "block"
- And I should not see "Category 1" in the "My courses" "block"
- And I should not see "Category 2" in the "My courses" "block"
- And I should not see "Category 3" in the "My courses" "block"
+ Then I should not see "Category 1" in the "My courses" "block"
+ And I should not see "Category A" in the "My courses" "block"
+ And I should not see "Category B" in the "My courses" "block"
+ And I should not see "Category C" in the "My courses" "block"
And I should see "Course 1" in the "My courses" "block"
And I should not see "Course 2" in the "My courses" "block"
And I follow "All courses"
- And I should see "Miscellaneous"
+ And I should see "Category 1"
Background:
Given the following "categories" exist:
| name | category | idnumber |
- | Category 1 | 0 | CAT1 |
- | Category 2 | 0 | CAT2 |
- | Category 3 | CAT2 | CAT3 |
+ | Category A | 0 | CATA |
+ | Category B | 0 | CATB |
+ | Category C | CATB | CATC |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
- | Course 2 | C2 | CAT1 |
- | Course 3 | C3 | CAT2 |
- | Course 4 | C4 | CAT3 |
+ | Course 2 | C2 | CATA |
+ | Course 3 | C3 | CATB |
+ | Course 4 | C4 | CATC |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | First | teacher1@example.com |
And I should see "Course 3" in the "My courses" "block"
And I should not see "Course 4" in the "My courses" "block"
And I follow "All courses"
- And I should see "Miscellaneous"
+ And I should see "Category 1"
Scenario: Add the course list block on the dashboard and navigate to another course
Given I log in as "teacher1"
Given I log in as "admin"
And I press "Customise this page"
When I add the "Courses" block
- Then I should see "Miscellaneous" in the "Course categories" "block"
- And I should see "Category 1" in the "Course categories" "block"
- And I should see "Category 2" in the "Course categories" "block"
- And I should not see "Category 3" in the "Course categories" "block"
+ Then I should see "Category 1" in the "Course categories" "block"
+ And I should see "Category A" in the "Course categories" "block"
+ And I should see "Category B" in the "Course categories" "block"
+ And I should not see "Category C" in the "Course categories" "block"
And I should not see "Course 1" in the "Course categories" "block"
And I should not see "Course 2" in the "Course categories" "block"
And I follow "All courses"
- And I should see "Miscellaneous"
+ And I should see "Category 1"
Background:
Given the following "categories" exist:
| name | category | idnumber |
- | Category 1 | 0 | CAT1 |
- | Category 2 | 0 | CAT2 |
- | Category 3 | CAT2 | CAT3 |
+ | Category A | 0 | CATA |
+ | Category B | 0 | CATB |
+ | Category C | CATB | CATC |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
- | Course 2 | C2 | CAT1 |
- | Course 3 | C3 | CAT2 |
- | Course 4 | C4 | CAT3 |
+ | Course 2 | C2 | CATA |
+ | Course 3 | C3 | CATB |
+ | Course 4 | C4 | CATC |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | First | teacher1@example.com |
And I should see "Course 3" in the "My courses" "block"
And I should not see "Course 4" in the "My courses" "block"
And I follow "All courses"
- And I should see "Miscellaneous"
+ And I should see "Category 1"
Scenario: Add the course list block on the frontpage page and navigate to another course
Given I log in as "admin"
And I am on site homepage
And I navigate to "Turn editing on" in current page administration
When I add the "Courses" block
- Then I should see "Miscellaneous" in the "Course categories" "block"
- And I should see "Category 1" in the "Course categories" "block"
- And I should see "Category 2" in the "Course categories" "block"
- And I should not see "Category 3" in the "Course categories" "block"
+ Then I should see "Category 1" in the "Course categories" "block"
+ And I should see "Category A" in the "Course categories" "block"
+ And I should see "Category B" in the "Course categories" "block"
+ And I should not see "Category C" in the "Course categories" "block"
And I should not see "Course 1" in the "Course categories" "block"
And I should not see "Course 2" in the "Course categories" "block"
And I follow "All courses"
- And I should see "Miscellaneous"
+ And I should see "Category 1"
Scenario: Add the course list block on the frontpage page and view as a guest
Given I log in as "admin"
And I add the "Courses" block
And I log out
When I log in as "guest"
- Then I should see "Miscellaneous" in the "Course categories" "block"
- And I should see "Category 1" in the "Course categories" "block"
- And I should see "Category 2" in the "Course categories" "block"
- And I should not see "Category 3" in the "Course categories" "block"
+ Then I should see "Category 1" in the "Course categories" "block"
+ And I should see "Category A" in the "Course categories" "block"
+ And I should see "Category B" in the "Course categories" "block"
+ And I should not see "Category C" in the "Course categories" "block"
And I should not see "Course 1" in the "Course categories" "block"
And I should not see "Course 2" in the "Course categories" "block"
And I follow "All courses"
- And I should see "Miscellaneous"
+ And I should see "Category 1"
"fullname": "course 3",
"hasprogress": true,
"progress": 10,
- "coursecategory": "Miscellaneous",
+ "coursecategory": "Category 1",
"visible": true
}
]
"fullname": "course 3",
"hasprogress": true,
"progress": 10,
- "coursecategory": "Miscellaneous",
+ "coursecategory": "Category 1",
"visible": true
}
]
"summary": "This course is about assignments",
"hasprogress": true,
"progress": 10,
- "coursecategory": "Miscellaneous",
+ "coursecategory": "Category 1",
"visible": true
}
]
| student1 | Student | 1 | student1@example.com |
And the following "categories" exist:
| name | category | idnumber |
- | Category 1 | 0 | CAT1 |
+ | Category A | 0 | CATA |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
| Course 2 | C2 | 0 |
| Course 3 | C3 | 0 |
- | Course 4 | C4 | CAT1 |
+ | Course 4 | C4 | CATA |
| Course 5 | C5 | 0 |
And the following "course enrolments" exist:
| user | course | role |
And I am on "Course 1" course homepage
And I am on "Course 4" course homepage
And I follow "Dashboard" in the user menu
- And I should see "Miscellaneous" in the "Recently accessed courses" "block"
And I should see "Category 1" in the "Recently accessed courses" "block"
+ And I should see "Category A" in the "Recently accessed courses" "block"
Scenario: Hide course category name
Given the following config values are set as admin:
And I am on "Course 1" course homepage
And I am on "Course 4" course homepage
And I follow "Dashboard" in the user menu
- And I should not see "Miscellaneous" in the "Recently accessed courses" "block"
And I should not see "Category 1" in the "Recently accessed courses" "block"
+ And I should not see "Category A" in the "Recently accessed courses" "block"
Scenario: Show short course name
Given the following config values are set as admin:
| Calendar name | Test Import |
| Import from | Calendar file (.ics) |
| Type of event | Category |
- | Category | Miscellaneous |
+ | Category | Category 1 |
And I upload "calendar/tests/fixtures/import.ics" file to "Calendar file (.ics)" filemanager
And I press "Import calendar"
And I should see "Category events"
// Change our event's date to the 20th Monday of the current year.
$twentiethmonday = new DateTime(date('Y-01-01'));
$twentiethmonday->modify('+20 Monday');
- $startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T090000'), 'US/Eastern');
-
- $startdate = new DateTime($startdatetime->format('Y-m-d'));
-
- $offset = $startdatetime->diff($startdate, true);
+ $startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'US/Eastern');
$interval = new DateInterval('P1Y');
$expecteddate->modify('January 1');
$expecteddate->add($interval);
$expecteddate->modify("+20 Monday");
- $expecteddate->add($offset);
}
}
| name | idnumber | description | Context | visible | Status |
| cohort name 1 | cohortid1 | first description | System | 1 | |
| cohort name 2 | cohortid2 | | System | 1 | |
- | cohort name 3 | cohortid3 | | Miscellaneous | 0 | |
+ | cohort name 3 | cohortid3 | | Category 1 | 0 | |
| cohort name 4 | cohortid4 | | Cat 1 | 1 | |
| cohort name 5 | cohortid5 | | Cat 2 | 0 | |
| cohort name 6 | cohortid6 | | Cat 3 | 1 | |
| Category | Name | Cohort ID | Description | Cohort size | Source |
| System | cohort name 1 | cohortid1 | first description | 0 | Created manually |
| System | cohort name 2 | cohortid2 | | 0 | Created manually |
- | Miscellaneous | cohort name 3 | cohortid3 | | 0 | Created manually |
+ | Category 1 | cohort name 3 | cohortid3 | | 0 | Created manually |
| Cat 1 | cohort name 4 | cohortid4 | | 0 | Created manually |
| Cat 2 | cohort name 5 | cohortid5 | | 0 | Created manually |
| Cat 3 | cohort name 6 | cohortid6 | | 0 | Created manually |
| name | idnumber | description | Context | Status |
| cohort name 1 | cohortid1 | first description | Cat 3 | |
| cohort name 2 | cohortid2 | | Cat 3 | |
- | cohort name 3 | cohortid3 | | Miscellaneous | |
+ | cohort name 3 | cohortid3 | | Category 1 | |
| cohort name 4 | cohortid4 | | Cat 1 | |
| cohort name 5 | cohortid5 | | Cat 2 | |
| cohort name 6 | cohortid6 | | Cat 3 | |
| Category | Name | Cohort ID | Description | Cohort size | Source |
| Cat 3 | cohort name 1 | cohortid1 | first description | 0 | Created manually |
| Cat 3 | cohort name 2 | cohortid2 | | 0 | Created manually |
- | Miscellaneous | cohort name 3 | cohortid3 | | 0 | Created manually |
+ | Category 1 | cohort name 3 | cohortid3 | | 0 | Created manually |
| Cat 1 | cohort name 4 | cohortid4 | | 0 | Created manually |
| Cat 2 | cohort name 5 | cohortid5 | | 0 | Created manually |
| Cat 3 | cohort name 6 | cohortid6 | | 0 | Created manually |
| name | idnumber | description | Context | Status |
| cohort name 1 | cohortid1 | first description | Cat 1 | |
| cohort name 2 | cohortid2 | | Cat 1 | |
- | cohort name 3 | cohortid3 | | Cat 1 | Category Miscellaneous not found or you don't have permission to create a cohort there. The default context will be used. |
+ | cohort name 3 | cohortid3 | | Cat 1 | Category Category 1 not found or you don't have permission to create a cohort there. The default context will be used. |
| cohort name 4 | cohortid4 | | Cat 1 | |
| cohort name 5 | cohortid5 | | Cat 1 | Category CAT2 not found or you don't have permission to create a cohort there. The default context will be used. |
| cohort name 6 | cohortid6 | | Cat 3 | |
| name | idnumber | description | Context | Status |
| cohort name 1 | cohortid1 | first description | System | |
| cohort name 2 | cohortid2 | | System | Cohort with the same ID number already exists |
- | cohort name 3 | cohortid3 | | Miscellaneous | |
+ | cohort name 3 | cohortid3 | | Category 1 | |
| cohort name 4 | cohortid4 | | Cat 1 | |
| cohort name 5 | cohortid5 | | Cat 2 | |
| cohort name 6 | cohortid6 | | Cat 3 | |
And I click on "Preview" "button"
Then the following should exist in the "previewuploadedcohorts" table:
| name | idnumber | description | Context | Status |
- | Specify category as name | cohortid1 | | Miscellaneous | |
+ | Specify category as name | cohortid1 | | Category 1 | |
| Specify category as idnumber | cohortid2 | | Cat 1 | |
- | Specify category as id | cohortid3 | | Miscellaneous | |
+ | Specify category as id | cohortid3 | | Category 1 | |
| Specify category as path | cohortid4 | | Cat 3 | |
- | Specify category_id | cohortid5 | | Miscellaneous | |
+ | Specify category_id | cohortid5 | | Category 1 | |
| Specify category_idnumber | cohortid6 | | Cat 1 | |
| Specify category_path | cohortid7 | | Cat 3 | |
And I should not see "not found or you"
| name | idnumber | description | Context | visible | theme | Status |
| cohort name 1 | cohortid1 | first description | System | 1 | boost | |
| cohort name 2 | cohortid2 | | System | 1 | | |
- | cohort name 3 | cohortid3 | | Miscellaneous | 0 | boost | |
+ | cohort name 3 | cohortid3 | | Category 1 | 0 | boost | |
| cohort name 4 | cohortid4 | | Cat 1 | 1 | classic | |
| cohort name 5 | cohortid5 | | Cat 2 | 0 | | |
| cohort name 6 | cohortid6 | | Cat 3 | 1 | classic | |
| Category | Name | Cohort ID | Description | Cohort size | Source |
| System | cohort name 1 | cohortid1 | first description | 0 | Created manually |
| System | cohort name 2 | cohortid2 | | 0 | Created manually |
- | Miscellaneous | cohort name 3 | cohortid3 | | 0 | Created manually |
+ | Category 1 | cohort name 3 | cohortid3 | | 0 | Created manually |
| Cat 1 | cohort name 4 | cohortid4 | | 0 | Created manually |
| Cat 2 | cohort name 5 | cohortid5 | | 0 | Created manually |
| Cat 3 | cohort name 6 | cohortid6 | | 0 | Created manually |
name,idnumber,description,category,visible
cohort name 1,cohortid1,first description,,
cohort name 2,cohortid2,,,
-cohort name 3,cohortid3,,Miscellaneous,no
+cohort name 3,cohortid3,,Category 1,no
cohort name 4,cohortid4,,CAT1,yes
cohort name 5,cohortid5,,CAT2,0
cohort name 6,cohortid6,,CAT3,1
name,idnumber,description,category,category_id,category_idnumber,category_path
-Specify category as name,cohortid1,,Miscellaneous,,,
+Specify category as name,cohortid1,,Category 1,,,
Specify category as idnumber,cohortid2,,CAT1,,,
Specify category as id,cohortid3,,1,,,
Specify category as path,cohortid4,,Cat 1 / Cat 3,,,
name,idnumber,description,category,visible,theme
cohort name 1,cohortid1,first description,,,boost
cohort name 2,cohortid2,,,,
-cohort name 3,cohortid3,,Miscellaneous,no,boost
+cohort name 3,cohortid3,,Category 1,no,boost
cohort name 4,cohortid4,,CAT1,yes,classic
cohort name 5,cohortid5,,CAT2,0,
cohort name 6,cohortid6,,CAT3,1,classic
name,idnumber,description,category
cohort name 1,cid1,first description,
cohort name 2,cid2,,
-cohort name 3,cid3,,Miscellaneous
+cohort name 3,cid3,,Category 1
cohort name 4,cid4,,CAT1
cohort name 5,cid5,,CAT2
cohort name 6,cid6,,CAT3
*/
public function __construct(completion_info $completioninfo, cm_info $cminfo, int $userid, bool $returndetails = true) {
$this->completioninfo = $completioninfo;
- $this->completiondata = $completioninfo->get_data($cminfo, false, $userid);
+ // We need to pass wholecourse = true here for better performance. All the course's completion data for the current
+ // logged-in user will get in a single query instead of multiple queries and loaded to cache.
+ $this->completiondata = $completioninfo->get_data($cminfo, true, $userid);
$this->cminfo = $cminfo;
$this->userid = $userid;
$this->returndetails = $returndetails;
// $CFG->behat_wwwroot = 'http://127.0.0.1/moodle';
// $CFG->behat_prefix = 'bht_';
// $CFG->behat_dataroot = '/home/example/bht_moodledata';
+// $CFG->behat_dbname = 'behat'; // optional
+// $CFG->behat_dbuser = 'username'; // optional
+// $CFG->behat_dbpass = 'password'; // optional
+// $CFG->behat_dbhost = 'localhost'; // optional
//
// You can override default Moodle configuration for Behat and add your own
// params; here you can add more profiles, use different Mink drivers than Selenium...
public function is_view_allowed(): bool {
// Force H5P content to be deployed.
$fileurl = $this->get_file_url();
+ if (empty($fileurl)) {
+ // This should never happen because H5P contents should have always a file. However, this extra-checked has been added
+ // to avoid the contentbank stop working if, for any unkonwn/weird reason, the file doesn't exist.
+ return false;
+ }
+
// Skip capability check when creating the H5P content (because it has been created by trusted users).
$h5pplayer = new \core_h5p\player($fileurl, new \stdClass(), true, '', true);
// Flush error messages.
*
* Used after d&d of the module to another section
*
- * @param {JQuery} activityElement
+ * @param {JQuery|Element} element
* @param {Number} cmid
* @param {Number} sectionreturn
*/
- var refreshModule = function(activityElement, cmid, sectionreturn) {
+ var refreshModule = function(element, cmid, sectionreturn) {
+ const activityElement = $(element);
var spinner = addActivitySpinner(activityElement);
var promises = ajax.call([{
methodname: 'core_course_get_module',
log.debug('replaceSectionActionItem() is deprecated and will be removed.');
var actionitem = sectionelement.find(SELECTOR.SECTIONACTIONMENU + ' ' + selector);
replaceActionItem(actionitem, image, stringname, stringcomponent, newaction);
- }
+ },
+ // Method to refresh a module.
+ refreshModule,
};
});
* @return string|bool Returns course image url as a string or false if the image is not exist
*/
public function load_for_cache($key) {
- $course = get_fast_modinfo($key)->get_course();
+ // We should use get_course() instead of get_fast_modinfo() for better performance.
+ $course = get_course($key);
return $this->get_image_url_from_overview_files($course);
}
// No categories found.
// This may happen after upgrade of a very old moodle version.
// In new versions the default category is created on install.
- $defcoursecat = self::create(array('name' => get_string('miscellaneous')));
+ $defcoursecat = self::create(array('name' => get_string('defaultcategoryname')));
set_config('defaultrequestcategory', $defcoursecat->id);
$all[0] = array($defcoursecat->id);
$all[$defcoursecat->id] = array();
* Returns ids of all parents of the category. Last element in the return array is the direct parent
*
* For example, if you have a tree of categories like:
- * Miscellaneous (id = 1)
+ * Category (id = 1)
* Subcategory (id = 2)
* Sub-subcategory (id = 4)
* Other category (id = 3)
* List is cached for 10 minutes
*
* For example, if you have a tree of categories like:
- * Miscellaneous (id = 1)
+ * Category (id = 1)
* Subcategory (id = 2)
* Sub-subcategory (id = 4)
* Other category (id = 3)
* Then after calling this function you will have
- * array(1 => 'Miscellaneous',
- * 2 => 'Miscellaneous / Subcategory',
- * 4 => 'Miscellaneous / Subcategory / Sub-subcategory',
+ * array(1 => 'Category',
+ * 2 => 'Category / Subcategory',
+ * 4 => 'Category / Subcategory / Sub-subcategory',
* 3 => 'Other category');
*
* If you specify $requiredcapability, then only categories where the current
* @param {number} courseId the course id
* @param {setup} setup format, page and course settings
* @property {boolean} setup.editing if the page is in edit mode
+ * @property {boolean} setup.supportscomponents if the format supports components for content
*/
export const setViewFormat = (courseId, setup) => {
const editor = getCourseEditor(courseId);
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course index main component.
+ *
+ * @module core_courseformat/local/content
+ * @class core_courseformat/local/content
+ * @copyright 2020 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent} from 'core/reactive';
+import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
+import inplaceeditable from 'core/inplace_editable';
+// Course actions is needed for actions that are not migrated to components.
+import courseActions from 'core_course/actions';
+
+export default class Component extends BaseComponent {
+
+ /**
+ * Constructor hook.
+ */
+ create() {
+ // Optional component name for debugging.
+ this.name = 'course_format';
+ // Default query selectors.
+ this.selectors = {
+ SECTION: `[data-for='section']`,
+ SECTION_ITEM: `[data-for='section_item']`,
+ SECTION_TITLE: `[data-for='section_title']`,
+ SECTION_CMLIST: `[data-for='cmlist']`,
+ COURSE_SECTIONLIST: `[data-for='course_sectionlist']`,
+ CM: `[data-for='cmitem']`,
+ };
+ // Array to save dettached elements during element resorting.
+ this.dettachedCms = {};
+ this.dettachedSections = {};
+ }
+
+ /**
+ * Static method to create a component instance form the mustahce template.
+ *
+ * @param {string} target the DOM main element or its ID
+ * @param {object} selectors optional css selector overrides
+ * @return {Component}
+ */
+ static init(target, selectors) {
+ return new Component({
+ element: document.getElementById(target),
+ reactive: getCurrentCourseEditor(),
+ selectors,
+ });
+ }
+
+ /**
+ * Return the component watchers.
+ *
+ * @returns {Array} of watchers
+ */
+ getWatchers() {
+ // Check if the course format is compatible with reactive components.
+ if (!this.reactive.supportComponents) {
+ return [];
+ }
+ return [
+ // State changes that require to reload some course modules.
+ {watch: `cm.visible:updated`, handler: this._reloadCm},
+ // Update section number and title.
+ {watch: `section.number:updated`, handler: this._refreshSectionNumber},
+ // Sections and cm sorting.
+ {watch: `transaction:start`, handler: this._startProcessing},
+ {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},
+ {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},
+ ];
+ }
+
+ /**
+ * Reload a course module.
+ *
+ * Most course module HTML is still strongly backend dependant.
+ * Some changes require to get a new version af the module.
+ *
+ * @param {Object} update the state update data
+ */
+ _reloadCm({element}) {
+ const cmitem = this.getElement(this.selectors.CM, element.id);
+ if (cmitem) {
+ courseActions.refreshModule(cmitem, element.id);
+ }
+ }
+
+ /**
+ * Setup the component to start a transaction.
+ *
+ * Some of the course actions replaces the current DOM element with a new one before updating the
+ * course state. This means the component cannot preload any index properly until the transaction starts.
+ *
+ */
+ _startProcessing() {
+ // During a section or cm sorting, some elements could be dettached from the DOM and we
+ // need to store somewhare in case they are needed later.
+ this.dettachedCms = {};
+ this.dettachedSections = {};
+ }
+
+ /**
+ * Update a course section when the section number changes.
+ *
+ * The courseActions module used for most course section tools still depends on css classes and
+ * section numbers (not id). To prevent inconsistencies when a section is moved, we need to refresh
+ * the
+ *
+ * Course formats can override the section title rendering so the frontend depends heavily on backend
+ * rendering. Luckily in edit mode we can trigger a title update using the inplace_editable module.
+ *
+ * @param {Object} details the update details.
+ */
+ _refreshSectionNumber({element}) {
+ // Find the element.
+ const target = this.getElement(this.selectors.SECTION, element.id);
+ if (!target) {
+ throw new Error(`Unkown section with ID ${element.id}`);
+ }
+ // Update section numbers in all data, css and YUI attributes.
+ target.id = `section-${element.number}`;
+ // YUI uses section number as section id in data-sectionid, in principle if a format use components
+ // don't need this sectionid attribute anymore, but we keep the compatibility in case some plugin
+ // use it for legacy purposes.
+ target.dataset.sectionid = element.number;
+ // The data-number is the attribute used by components to store the section number.
+ target.dataset.number = element.number;
+
+ // Update title and title inplace editable, if any.
+ const inplace = inplaceeditable.getInplaceEditable(target.querySelector(this.selectors.SECTION_TITLE));
+ if (inplace) {
+ // The course content HTML can be modified at any moment, so the function need to do some checkings
+ // to make sure the inplace editable still represents the same itemid.
+ const currentvalue = inplace.getValue();
+ const currentitemid = inplace.getItemId();
+ // Unnamed sections must be recalculated.
+ if (inplace.getValue() === '') {
+ // The value to send can be an empty value if it is a default name.
+ if (currentitemid == element.id && (currentvalue != element.rawtitle || element.rawtitle == '')) {
+ inplace.setValue(element.rawtitle);
+ }
+ }
+ }
+ }
+
+ /**
+ * Refresh a section cm list.
+ *
+ * @param {Object} details the update details.
+ */
+ _refreshSectionCmlist({element}) {
+ const cmlist = element.cmlist ?? [];
+ const section = this.getElement(this.selectors.SECTION, element.id);
+ const listparent = section?.querySelector(this.selectors.SECTION_CMLIST);
+ if (listparent) {
+ this._fixOrder(listparent, cmlist, this.selectors.CM, this.dettachedCms);
+ }
+ }
+
+ /**
+ * Refresh the section list.
+ *
+ * @param {Object} details the update details.
+ */
+ _refreshCourseSectionlist({element}) {
+ const sectionlist = element.sectionlist ?? [];
+ const listparent = this.getElement(this.selectors.COURSE_SECTIONLIST);
+ if (listparent) {
+ this._fixOrder(listparent, sectionlist, this.selectors.SECTION, this.dettachedSections);
+ }
+ }
+
+ /**
+ * Fix/reorder the section or cms order.
+ *
+ * @param {Element} container the HTML element to reorder.
+ * @param {Array} neworder an array with the ids order
+ * @param {string} selector the element selector
+ * @param {Object} dettachedelements a list of dettached elements
+ */
+ _fixOrder(container, neworder, selector, dettachedelements) {
+
+ // Empty lists should not be visible.
+ if (!neworder.length) {
+ container.classList.add('hidden');
+ container.innerHTML = '';
+ return;
+ }
+
+ // Grant the list is visible (in case it was empty).
+ container.classList.remove('hidden');
+
+ // Move the elements in order at the beginning of the list.
+ neworder.forEach((itemid, index) => {
+ const item = this.getElement(selector, itemid) ?? dettachedelements[itemid];
+ // Get the current elemnt at that position.
+ const currentitem = container.children[index];
+ if (currentitem === undefined) {
+ container.append(item);
+ return;
+ }
+ if (currentitem !== item) {
+ container.insertBefore(item, currentitem);
+ }
+ });
+ // Remove the remaining elements.
+ while (container.children.length > neworder.length) {
+ const lastchild = container.lastChild;
+ dettachedelements[lastchild?.dataset?.id ?? 0] = lastchild;
+ container.removeChild(lastchild);
+ }
+ }
+}
// Default view format setup.
this._editing = false;
+ this._supportscomponents = false;
this.courseId = courseId;
*
* @param {Object} setup format, page and course settings
* @property {boolean} setup.editing if the page is in edit mode
+ * @property {boolean} setup.supportscomponents if the format supports components for content
*/
setViewFormat(setup) {
this._editing = setup.editing ?? false;
+ this._supportscomponents = setup.supportscomponents ?? false;
}
/**
return new Exporter(this);
}
+ /**
+ * Return if the current course support components to refresh the content.
+ *
+ * @returns {boolean} if the current content support components
+ */
+ get supportComponents() {
+ return this._supportscomponents ?? false;
+ }
+
/**
* Dispatch a change in the state.
*
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course index cm component.
+ *
+ * This component is used to control specific course modules interactions like drag and drop
+ * in both course index and course content.
+ *
+ * @module core_courseformat/local/courseeditor/dndcmitem
+ * @class core_courseformat/local/courseeditor/dndcmitem
+ * @copyright 2021 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent, DragDrop} from 'core/reactive';
+
+export default class extends BaseComponent {
+
+ /**
+ * Configure the component drag and drop.
+ *
+ * @param {number} cmid course module id
+ */
+ configDragDrop(cmid) {
+
+ this.id = cmid;
+
+ // Drag and drop is only available for components compatible course formats.
+ if (this.reactive.isEditing && this.reactive.supportComponents) {
+ // Init element drag and drop.
+ this.dragdrop = new DragDrop(this);
+ // Save dropzone classes.
+ this.classes = this.dragdrop.getClasses();
+ }
+ }
+
+ /**
+ * Remove all subcomponents dependencies.
+ */
+ destroy() {
+ if (this.dragdrop !== undefined) {
+ this.dragdrop.unregister();
+ }
+ }
+
+ // Drag and drop methods.
+
+ /**
+ * Get the draggable data of this component.
+ *
+ * @returns {Object} exported course module drop data
+ */
+ getDraggableData() {
+ const exporter = this.reactive.getExporter();
+ return exporter.cmDraggableData(this.reactive.state, this.id);
+ }
+
+ /**
+ * Validate if the drop data can be dropped over the component.
+ *
+ * @param {Object} dropdata the exported drop data.
+ * @returns {boolean}
+ */
+ validateDropData(dropdata) {
+ return dropdata?.type === 'cm';
+ }
+
+ /**
+ * Display the component dropzone.
+ *
+ * @param {Object} dropdata the accepted drop data
+ */
+ showDropZone(dropdata) {
+ // If we are the next cmid of the dragged element we accept the drop because otherwise it
+ // will get captured by the section. However, we won't trigger any mutation.
+ if (dropdata.nextcmid != this.id && dropdata.id != this.id) {
+ this.element.classList.add(this.classes.DROPUP);
+ }
+ }
+
+ /**
+ * Hide the component dropzone.
+ */
+ hideDropZone() {
+ this.element.classList.remove(this.classes.DROPUP);
+ }
+
+ /**
+ * Drop event handler.
+ *
+ * @param {Object} dropdata the accepted drop data
+ */
+ drop(dropdata) {
+ // Call the move mutation if necessary.
+ if (dropdata.id != this.id && dropdata.nextcmid != this.id) {
+ this.reactive.dispatch('cmMove', [dropdata.id], null, this.id);
+ }
+ }
+
+}
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course index section component.
+ *
+ * This component is used to control specific course section interactions like drag and drop
+ * in both course index and course content.
+ *
+ * @module core_courseformat/local/courseeditor/dndsection
+ * @class core_courseformat/local/courseeditor/dndsection
+ * @copyright 2021 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent, DragDrop} from 'core/reactive';
+
+export default class extends BaseComponent {
+
+ /**
+ * Save some values form the state.
+ *
+ * @param {Object} state the current state
+ */
+ configState(state) {
+ this.id = this.element.dataset.id;
+ this.section = state.section.get(this.id);
+ this.course = state.course;
+ }
+
+ /**
+ * Register state values and the drag and drop subcomponent.
+ *
+ * @param {BaseComponent} sectionitem section item component
+ */
+ configDragDrop(sectionitem) {
+ // Drag and drop is only available for components compatible course formats.
+ if (this.reactive.isEditing && this.reactive.supportComponents) {
+ // Init the inner dragable element.
+ this.sectionitem = sectionitem;
+ // Init the dropzone.
+ this.dragdrop = new DragDrop(this);
+ // Save dropzone classes.
+ this.classes = this.dragdrop.getClasses();
+ }
+ }
+
+ /**
+ * Remove all subcomponents dependencies.
+ */
+ destroy() {
+ if (this.sectionitem !== undefined) {
+ this.sectionitem.unregister();
+ }
+ if (this.dragdrop !== undefined) {
+ this.dragdrop.unregister();
+ }
+ }
+
+ /**
+ * Get the last CM element of that section.
+ *
+ * @returns {element|null} the las course module element of the section.
+ */
+ getLastCm() {
+ return null;
+ }
+
+ // Drag and drop methods.
+
+ /**
+ * Validate if the drop data can be dropped over the component.
+ *
+ * @param {Object} dropdata the exported drop data.
+ * @returns {boolean}
+ */
+ validateDropData(dropdata) {
+ // We accept any course module.
+ if (dropdata?.type === 'cm') {
+ return true;
+ }
+ // We accept any section bu the section 0 or ourself
+ if (dropdata?.type === 'section') {
+ const sectionzeroid = this.course.sectionlist[0];
+ return dropdata?.id != this.id && dropdata?.id != sectionzeroid && this.id != sectionzeroid;
+ }
+ return false;
+ }
+
+ /**
+ * Display the component dropzone.
+ *
+ * @param {Object} dropdata the accepted drop data
+ */
+ showDropZone(dropdata) {
+ if (dropdata.type == 'cm') {
+ this.getLastCm()?.classList.add(this.classes.DROPDOWN);
+ }
+ if (dropdata.type == 'section') {
+ // The relative move of section depends on the section number.
+ if (this.section.number > dropdata.number) {
+ this.element.classList.remove(this.classes.DROPUP);
+ this.element.classList.add(this.classes.DROPDOWN);
+ } else {
+ this.element.classList.add(this.classes.DROPUP);
+ this.element.classList.remove(this.classes.DROPDOWN);
+ }
+ }
+ }
+
+ /**
+ * Hide the component dropzone.
+ */
+ hideDropZone() {
+ this.getLastCm()?.classList.remove(this.classes.DROPDOWN);
+ this.element.classList.remove(this.classes.DROPUP);
+ this.element.classList.remove(this.classes.DROPDOWN);
+ }
+
+ /**
+ * Drop event handler.
+ *
+ * @param {Object} dropdata the accepted drop data
+ */
+ drop(dropdata) {
+ // Call the move mutation.
+ if (dropdata.type == 'cm') {
+ this.reactive.dispatch('cmMove', [dropdata.id], this.id);
+ }
+ if (dropdata.type == 'section') {
+ this.reactive.dispatch('sectionMove', [dropdata.id], this.id);
+ }
+ }
+}
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course index section title draggable component.
+ *
+ * This component is used to control specific course section interactions like drag and drop
+ * in both course index and course content.
+ *
+ * @module core_courseformat/local/courseeditor/dndsectionitem
+ * @class core_courseformat/local/courseeditor/dndsectionitem
+ * @copyright 2021 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent, DragDrop} from 'core/reactive';
+
+export default class extends BaseComponent {
+
+ /**
+ * Initial state ready method.
+ *
+ * @param {number} sectionid the section id
+ * @param {Object} state the initial state
+ * @param {Element} fullregion the complete section region to mark as dragged
+ */
+ configDragDrop(sectionid, state, fullregion) {
+
+ this.id = sectionid;
+ if (this.section === undefined) {
+ this.section = state.section.get(this.id);
+ }
+ if (this.course === undefined) {
+ this.course = state.course;
+ }
+
+ // Prevent topic zero from being draggable.
+ if (this.section.number > 0) {
+ this.getDraggableData = this._getDraggableData;
+ }
+
+ this.fullregion = fullregion;
+
+ // Drag and drop is only available for components compatible course formats.
+ if (this.reactive.isEditing && this.reactive.supportComponents) {
+ // Init the dropzone.
+ this.dragdrop = new DragDrop(this);
+ // Save dropzone classes.
+ this.classes = this.dragdrop.getClasses();
+ }
+ }
+
+ /**
+ * Remove all subcomponents dependencies.
+ */
+ destroy() {
+ if (this.dragdrop !== undefined) {
+ this.dragdrop.unregister();
+ }
+ }
+
+ // Drag and drop methods.
+
+ /**
+ * Get the draggable data of this component.
+ *
+ * @returns {Object} exported course module drop data
+ */
+ _getDraggableData() {
+ const exporter = this.reactive.getExporter();
+ return exporter.sectionDraggableData(this.reactive.state, this.id);
+ }
+
+ /**
+ * Validate if the drop data can be dropped over the component.
+ *
+ * @param {Object} dropdata the exported drop data.
+ * @returns {boolean}
+ */
+ validateDropData(dropdata) {
+ // Course module validation.
+ if (dropdata?.type === 'cm') {
+ // The first section element is already there so we can ignore it.
+ const firstcmid = this.section?.cmlist[0];
+ return dropdata.id !== firstcmid;
+ }
+ return false;
+ }
+
+ /**
+ * Display the component dropzone.
+ *
+ * @param {Object} dropdata the accepted drop data
+ */
+ showDropZone() {
+ this.element.classList.add(this.classes.DROPZONE);
+ }
+
+ /**
+ * Hide the component dropzone.
+ */
+ hideDropZone() {
+ this.element.classList.remove(this.classes.DROPZONE);
+ }
+
+ /**
+ * Drop event handler.
+ *
+ * @param {Object} dropdata the accepted drop data
+ */
+ drop(dropdata) {
+ // Call the move mutation.
+ if (dropdata.type == 'cm') {
+ this.reactive.dispatch('cmMove', [dropdata.id], this.id, this.section?.cmlist[0]);
+ }
+ }
+}
};
return cm;
}
+
+ /**
+ * Generate a dragable cm data structure.
+ *
+ * This method is used by any draggable course module element to generate drop data
+ * for its reactive/dragdrop instance.
+ *
+ * @param {*} state the state object
+ * @param {*} cmid the cours emodule id
+ * @returns {Object|null}
+ */
+ cmDraggableData(state, cmid) {
+ const cminfo = state.cm.get(cmid);
+ if (!cminfo) {
+ return null;
+ }
+
+ // Drop an activity over the next activity is the same as doing anything.
+ let nextcmid;
+ const section = state.section.get(cminfo.sectionid);
+ const currentindex = section?.cmlist.indexOf(cminfo.id);
+ if (currentindex !== undefined) {
+ nextcmid = section?.cmlist[currentindex + 1];
+ }
+
+ return {
+ type: 'cm',
+ id: cminfo.id,
+ name: cminfo.name,
+ nextcmid,
+ };
+ }
+
+ /**
+ * Generate a dragable cm data structure.
+ *
+ * This method is used by any draggable section element to generate drop data
+ * for its reactive/dragdrop instance.
+ *
+ * @param {*} state the state object
+ * @param {*} sectionid the cours section id
+ * @returns {Object|null}
+ */
+ sectionDraggableData(state, sectionid) {
+ const sectioninfo = state.section.get(sectionid);
+ if (!sectioninfo) {
+ return null;
+ }
+ return {
+ type: 'section',
+ id: sectioninfo.id,
+ name: sectioninfo.name,
+ number: sectioninfo.number,
+ };
+ }
}
* @param {string} action
* @param {number} courseId
* @param {array} ids
+ * @param {number} targetSectionId optional target section id (for moving actions)
+ * @param {number} targetCmId optional target cm id (for moving actions)
*/
- async _callEditWebservice(action, courseId, ids) {
+ async _callEditWebservice(action, courseId, ids, targetSectionId, targetCmId) {
+ const args = {
+ action,
+ courseid: courseId,
+ ids,
+ };
+ if (targetSectionId) {
+ args.targetsectionid = targetSectionId;
+ }
+ if (targetCmId) {
+ args.targetcmid = targetCmId;
+ }
let ajaxresult = await ajax.call([{
methodname: 'core_courseformat_update_course',
- args: {
- action,
- courseid: courseId,
- ids,
- }
+ args,
}])[0];
return JSON.parse(ajaxresult);
}
+ /**
+ * Move course modules to specific course location.
+ *
+ * Note that one of targetSectionId or targetCmId should be provided in order to identify the
+ * new location:
+ * - targetCmId: the activities will be located avobe the target cm. The targetSectionId
+ * value will be ignored in this case.
+ * - targetSectionId: the activities will be appended to the section. In this case
+ * targetSectionId should not be present.
+ *
+ * @param {StateManager} stateManager the current state manager
+ * @param {array} cmids the list of cm ids to move
+ * @param {number} targetSectionId the target section id
+ * @param {number} targetCmId the target course module id
+ */
+ async cmMove(stateManager, cmids, targetSectionId, targetCmId) {
+ if (!targetSectionId && !targetCmId) {
+ throw new Error(`Mutation cmMove requires targetSectionId or targetCmId`);
+ }
+ const course = stateManager.get('course');
+ const updates = await this._callEditWebservice('cm_move', course.id, cmids, targetSectionId, targetCmId);
+ stateManager.processUpdates(updates);
+ }
+
+ /**
+ * Move course modules to specific course location.
+ *
+ * @param {StateManager} stateManager the current state manager
+ * @param {array} sectionIds the list of section ids to move
+ * @param {number} targetSectionId the target section id
+ */
+ async sectionMove(stateManager, sectionIds, targetSectionId) {
+ if (!targetSectionId) {
+ throw new Error(`Mutation sectionMove requires targetSectionId`);
+ }
+ const course = stateManager.get('course');
+ const updates = await this._callEditWebservice('section_move', course.id, sectionIds, targetSectionId);
+ stateManager.processUpdates(updates);
+ }
+
/**
* Get updated state data related to some cm ids.
*
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-import {BaseComponent} from 'core/reactive';
+import DndCmItem from 'core_courseformat/local/courseeditor/dndcmitem';
-export default class Component extends BaseComponent {
+export default class Component extends DndCmItem {
/**
* Constructor hook.
create() {
// Optional component name for debugging.
this.name = 'courseindex_cm';
- // Default query selectors.
- this.selectors = {
- };
// We need our id to watch specific events.
this.id = this.element.dataset.id;
}
* Initial state ready method.
*/
stateReady() {
- // Activate drag and drop soon.
+ this.configDragDrop(this.id);
}
+ /**
+ * Component watchers.
+ *
+ * @returns {Array} of watchers
+ */
getWatchers() {
return [
{watch: `cm[${this.id}]:deleted`, handler: this.remove},
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course index section component.
+ *
+ * This component is used to control specific course section interactions like drag and drop.
+ *
+ * @module core_courseformat/local/courseindex/section
+ * @class core_courseformat/local/courseindex/section
+ * @copyright 2021 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import SectionTitle from 'core_courseformat/local/courseindex/sectiontitle';
+import DndSection from 'core_courseformat/local/courseeditor/dndsection';
+
+export default class Component extends DndSection {
+
+ /**
+ * Constructor hook.
+ */
+ create() {
+ // Optional component name for debugging.
+ this.name = 'courseindex_section';
+ // Default query selectors.
+ this.selectors = {
+ SECTION_ITEM: `[data-for='section_item']`,
+ CM_LAST: `[data-for="cm"]:last-child`,
+ };
+ }
+
+ /**
+ * Static method to create a component instance form the mustahce template.
+ *
+ * @param {string} target the DOM main element or its ID
+ * @param {object} selectors optional css selector overrides
+ * @return {Component}
+ */
+ static init(target, selectors) {
+ return new Component({
+ element: document.getElementById(target),
+ selectors,
+ });
+ }
+
+ /**
+ * Initial state ready method.
+ *
+ * @param {Object} state the initial state
+ */
+ stateReady(state) {
+ this.configState(state);
+ // Drag and drop is only available for components compatible course formats.
+ if (this.reactive.isEditing && this.reactive.supportComponents) {
+ // Init the inner dragable element passing the full section as affected region.
+ const titleitem = new SectionTitle({
+ ...this,
+ element: this.getElement(this.selectors.SECTION_ITEM),
+ fullregion: this.element,
+ });
+ this.configDragDrop(titleitem);
+ }
+ }
+
+ /**
+ * Get the last CM element of that section.
+ *
+ * @returns {element|null}
+ */
+ getLastCm() {
+ return this.getElement(this.selectors.CM_LAST);
+ }
+}
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course index section title component.
+ *
+ * This component is used to control specific course section interactions like drag and drop.
+ *
+ * @module core_courseformat/local/courseindex/sectiontitle
+ * @class core_courseformat/local/courseindex/sectiontitle
+ * @copyright 2021 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import DndSectionItem from 'core_courseformat/local/courseeditor/dndsectionitem';
+
+export default class Component extends DndSectionItem {
+
+ /**
+ * Constructor hook.
+ *
+ * @param {Object} descriptor
+ */
+ create(descriptor) {
+ // Optional component name for debugging.
+ this.name = 'courseindex_sectiontitle';
+
+ this.id = descriptor.id;
+ this.section = descriptor.section;
+ this.course = descriptor.course;
+ this.fullregion = descriptor.fullregion;
+
+ // Prevent topic zero from being draggable.
+ if (this.section.number > 0) {
+ this.getDraggableData = this._getDraggableData;
+ }
+ }
+
+ /**
+ * Static method to create a component instance form the mustahce template.
+ *
+ * @param {element|string} target the DOM main element or its ID
+ * @param {object} selectors optional css selector overrides
+ * @return {Component}
+ */
+ static init(target, selectors) {
+ return new Component({
+ element: document.getElementById(target),
+ selectors,
+ });
+ }
+
+ /**
+ * Initial state ready method.
+ *
+ * @param {Object} state the initial state
+ */
+ stateReady(state) {
+ this.configDragDrop(this.id, state, this.fullregion);
+ }
+}
return $ajaxsupport;
}
+ /**
+ * Returns true if this course format is compatible with content components.
+ *
+ * Using components means the content elements can watch the frontend course state and
+ * react to the changes. Formats with component compatibility can have more interactions
+ * without refreshing the page, like having drag and drop from the course index to reorder
+ * sections and activities.
+ *
+ * @return bool if the format is compatible with components.
+ */
+ public function supports_components() {
+ return false;
+ }
+
+
/**
* Custom action after section has been moved in AJAX mode
*
'section' => $section->section,
'number' => $section->section,
'title' => $format->get_section_name($section),
+ 'rawtitle' => $section->name,
'cmlist' => [],
'visible' => !empty($section->visible),
'sectionurl' => course_get_url($course, $section->section)->out(),
namespace core_courseformat;
use core_courseformat\stateupdates;
+use cm_info;
+use section_info;
use stdClass;
use course_modinfo;
use moodle_exception;
+use context_module;
+use context_course;
/**
* Contains the core course state actions.
*/
class stateactions {
+ /**
+ * Move course modules to another location in the same course.
+ *
+ * @param stateupdates $updates the affected course elements track
+ * @param stdClass $course the course object
+ * @param int[] $ids the list of affected course module ids
+ * @param int $targetsectionid optional target section id
+ * @param int $targetcmid optional target cm id
+ */
+ public function cm_move(
+ stateupdates $updates,
+ stdClass $course,
+ array $ids,
+ ?int $targetsectionid = null,
+ ?int $targetcmid = null
+ ): void {
+ // Validate target elements.
+ if (!$targetsectionid && !$targetcmid) {
+ throw new moodle_exception("Action cm_move requires targetsectionid or targetcmid");
+ }
+
+ $this->validate_cms($course, $ids, __FUNCTION__);
+
+ // Check capabilities on every activity context.
+ foreach ($ids as $cmid) {
+ $modcontext = context_module::instance($cmid);
+ require_capability('moodle/course:manageactivities', $modcontext);
+ }
+
+ $modinfo = get_fast_modinfo($course);
+
+ // Target cm has more priority than target section.
+ if (!empty($targetcmid)) {
+ $this->validate_cms($course, [$targetcmid], __FUNCTION__);
+ $targetcm = $modinfo->get_cm($targetcmid);
+ $targetsection = $modinfo->get_section_info_by_id($targetcm->section, MUST_EXIST);
+ } else {
+ $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
+ $targetcm = null;
+ $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
+ }
+
+ // The origin sections must be updated as well.
+ $originalsections = [];
+
+ $cms = $this->get_cm_info($modinfo, $ids);
+ foreach ($cms as $cm) {
+ $currentsection = $modinfo->get_section_info_by_id($cm->section, MUST_EXIST);
+ moveto_module($cm, $targetsection, $targetcm);
+ $updates->add_cm_put($cm->id);
+ if ($currentsection->id != $targetsection->id) {
+ $originalsections[$currentsection->id] = true;
+ }
+ // If some of the original sections are also target sections, we don't need to update them.
+ if (array_key_exists($targetsection->id, $originalsections)) {
+ unset($originalsections[$targetsection->id]);
+ }
+ }
+
+ // Use section_state to return the full affected section and activities updated state.
+ $this->cm_state($updates, $course, $ids, $targetsectionid, $targetcmid);
+
+ foreach (array_keys($originalsections) as $sectionid) {
+ $updates->add_section_put($sectionid);
+ }
+ }
+
+ /**
+ * Move course sections to another location in the same course.
+ *
+ * @param stateupdates $updates the affected course elements track
+ * @param stdClass $course the course object
+ * @param int[] $ids the list of affected course module ids
+ * @param int $targetsectionid optional target section id
+ * @param int $targetcmid optional target cm id
+ */
+ public function section_move(
+ stateupdates $updates,
+ stdClass $course,
+ array $ids,
+ ?int $targetsectionid = null,
+ ?int $targetcmid = null
+ ): void {
+ // Validate target elements.
+ if (!$targetsectionid) {
+ throw new moodle_exception("Action cm_move requires targetsectionid");
+ }
+
+ $this->validate_sections($course, $ids, __FUNCTION__);
+
+ $coursecontext = context_course::instance($course->id);
+ require_capability('moodle/course:movesections', $coursecontext);
+
+ $modinfo = get_fast_modinfo($course);
+
+ // Target section.
+ $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
+ $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
+
+ $affectedsections = [$targetsection->section => true];
+
+ $sections = $this->get_section_info($modinfo, $ids);
+ foreach ($sections as $section) {
+ $affectedsections[$section->section] = true;
+ move_section_to($course, $section->section, $targetsection->section);
+ }
+
+ // Use section_state to return the section and activities updated state.
+ $this->section_state($updates, $course, $ids, $targetsectionid);
+
+ // All course sections can be renamed because of the resort.
+ $allsections = $modinfo->get_section_info_all();
+ foreach ($allsections as $section) {
+ // Ignore the affected sections because they are already in the updates.
+ if (isset($affectedsections[$section->section])) {
+ continue;
+ }
+ $updates->add_section_put($section->id);
+ }
+ // The section order is at a course level.
+ $updates->add_course_put();
+ }
+
+ /**
+ * Extract several cm_info from the course_modinfo.
+ *
+ * @param course_modinfo $modinfo the course modinfo.
+ * @param int[] $ids the course modules $ids
+ * @return cm_info[] the extracted cm_info objects
+ */
+ protected function get_cm_info (course_modinfo $modinfo, array $ids): array {
+ $cms = [];
+ foreach ($ids as $cmid) {
+ $cms[$cmid] = $modinfo->get_cm($cmid);
+ }
+ return $cms;
+ }
+
+ /**
+ * Extract several section_info from the course_modinfo.
+ *
+ * @param course_modinfo $modinfo the course modinfo.
+ * @param int[] $ids the course modules $ids
+ * @return section_info[] the extracted section_info objects
+ */
+ protected function get_section_info(course_modinfo $modinfo, array $ids): array {
+ $sections = [];
+ foreach ($ids as $sectionid) {
+ $sections[$sectionid] = $modinfo->get_section_info_by_id($sectionid);
+ }
+ return $sections;
+ }
+
/**
* Add the update messages of the updated version of any cm and section related to the cm ids.
*
}
}
}}
-<h2 class="accesshide">{{{title}}}</h2>
-{{{completionhelp}}}
-<ul class="{{format}}">
- {{#initialsection}}
- {{> core_courseformat/local/content/section }}
- {{/initialsection}}
- {{#sections}}
- {{> core_courseformat/local/content/section }}
- {{/sections}}
-</ul>
-{{#hasnavigation}}
-<div class="single-section">
- {{#sectionnavigation}} {{> core_courseformat/local/content/sectionnavigation }} {{/sectionnavigation}}
- <ul class="{{format}}">
- {{#singlesection}}
- {{> core_courseformat/local/content/section }}
- {{/singlesection}}
+<div id="{{uniqid}}-course-format">
+ <h2 class="accesshide">{{{title}}}</h2>
+ {{{completionhelp}}}
+ <ul class="{{format}}" data-for="course_sectionlist">
+ {{#initialsection}}
+ {{> core_courseformat/local/content/section }}
+ {{/initialsection}}
+ {{#sections}}
+ {{> core_courseformat/local/content/section }}
+ {{/sections}}
</ul>
- {{#sectionselector}} {{> core_courseformat/local/content/sectionselector }} {{/sectionselector}}
+ {{#hasnavigation}}
+ <div class="single-section">
+ {{#sectionnavigation}} {{> core_courseformat/local/content/sectionnavigation }} {{/sectionnavigation}}
+ <ul class="{{format}}">
+ {{#singlesection}}
+ {{> core_courseformat/local/content/section }}
+ {{/singlesection}}
+ </ul>
+ {{#sectionselector}} {{> core_courseformat/local/content/sectionselector }} {{/sectionselector}}
+ </div>
+ {{/hasnavigation}}
+ {{#numsections}} {{> core_courseformat/local/content/addsection}} {{/numsections}}
</div>
-{{/hasnavigation}}
-{{#numsections}} {{> core_courseformat/local/content/addsection}} {{/numsections}}
+{{#js}}
+require(['core_courseformat/local/content'], function(component) {
+ component.init('{{uniqid}}-course-format');
+});
+{{/js}}
}
}}
<li id="section-{{num}}"
- class="section main {{#onlysummary}} section-summary {{/onlysummary}} clearfix
- {{#ishidden}} hidden {{/ishidden}} {{#iscurrent}} current {{/iscurrent}}
- {{#isstealth}} orphaned {{/isstealth}}"
- role="region"
- aria-labelledby="sectionid-{{id}}-title"
- data-sectionid="{{num}}"
- data-sectionreturnid="{{sectionreturnid}}">
+ class="section main {{#onlysummary}} section-summary {{/onlysummary}} clearfix
+ {{#ishidden}} hidden {{/ishidden}} {{#iscurrent}} current {{/iscurrent}}
+ {{#isstealth}} orphaned {{/isstealth}}"
+ role="region"
+ aria-labelledby="sectionid-{{id}}-title"
+ data-sectionid="{{num}}"
+ data-sectionreturnid="{{sectionreturnid}}"
+ data-for="section"
+ data-id="{{id}}"
+ data-number="{{num}}"
+>
{{#singleheader}} {{> core_courseformat/local/content/section/header }} {{/singleheader}}
<div class="left side">{{#iscurrent}} {{{currentlink}}} {{/iscurrent}}</div>
<div class="right side">
"extraclasses": "newmessages"
}
}}
-<li class="activity {{module}} modtype_{{module}} {{extraclasses}} {{#hasinfo}}hasinfo{{/hasinfo}}" id="module-{{id}}">
+<li
+ class="activity {{module}} modtype_{{module}} {{extraclasses}} {{#hasinfo}}hasinfo{{/hasinfo}}"
+ id="module-{{id}}"
+ data-for="cmitem"
+ data-id="{{id}}"
+>
{{#cmformat}}
{{> core_courseformat/local/content/cm}}
{{/cmformat}}
{{#showmovehere}}
<p>{{movingstr}} (<a href="{{{cancelcopyurl}}}">{{#str}} cancel {{/str}}</a>)</p>
{{/showmovehere}}
-{{#hascms}}
- <ul class="section img-text">
- {{#cms}}
- {{#showmovehere}}
- <li class="movehere">
- <a href="{{{moveurl}}}" title="{{strmovefull}}" class="movehere"></a>
- </li>
- {{/showmovehere}}
- {{#cmitem}}
- {{> core_courseformat/local/content/section/cmitem}}
- {{/cmitem}}
- {{/cms}}
+<ul class="section img-text {{#hascms}} d-block {{/hascms}}" data-for="cmlist">
+{{#cms}}
{{#showmovehere}}
<li class="movehere">
- <a href="{{{movetosectionurl}}}" title="{{strmovefull}}" class="movehere"></a>
+ <a href="{{{moveurl}}}" title="{{strmovefull}}" class="movehere"></a>
</li>
{{/showmovehere}}
+ {{#cmitem}}
+ {{> core_courseformat/local/content/section/cmitem}}
+ {{/cmitem}}
+{{/cms}}
+{{#showmovehere}}
+ <li class="movehere">
+ <a href="{{{movetosectionurl}}}" title="{{strmovefull}}" class="movehere"></a>
+ </li>
+{{/showmovehere}}
</ul>
-{{/hascms}}
}
}}
-<h3 class="sectionid-{{id}}-title sectionname">
+<h3 class="sectionid-{{id}}-title sectionname" data-for="section_title">
{{#url}}
<a href="{{{url}}}" class="{{#ishidden}} dimmed_text {{/ishidden}}">{{name}}</a>
{{/url}}
{{{name}}}
</span>
{{/url}}
+ <span class="dragicon ml-auto">{{#pix}}i/dragdrop{{/pix}}</span>
</li>
{{#js}}
require(['core_courseformat/local/courseindex/cm'], function(component) {
>
{{{title}}}
</a>
+ <span class="dragicon ml-auto">{{#pix}}i/dragdrop{{/pix}}</span>
</div>
<div id="courseindexcollapse{{number}}"
class="courseindex-item-content collapse {{#isactive}}show{{/isactive}}"
</ul>
</div>
</div>
+{{#js}}
+require(['core_courseformat/local/courseindex/section'], function(component) {
+ component.init('{{uniqid}}-course-index-section-{{id}}');
+});
+{{/js}}
return $ajaxsupport;
}
+ public function supports_components() {
+ return true;
+ }
+
/**
* Loads all of the course sections into the navigation.
*
Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
=== 4.0 ===
-* New core_courseformat\uses_course_index() to define whether the course format uses course index or not.
+* New core_courseformat\base::uses_course_index() to define whether the course format uses course index or not.
+* New core_courseformat\base::supports_components() to specify if the format is compatible with reactive components.
=== 3.10 ===
* Added the missing callback supports_ajax() to format_social.
return $ajaxsupport;
}
+ public function supports_components() {
+ return true;
+ }
+
/**
* Loads all of the course sections into the navigation
*
// Edition mode and some format specs must be passed to the init method.
$setup = (object)[
'editing' => $format->show_editor(),
+ 'supportscomponents' => $format->supports_components(),
];
// All the new editor elements will be loaded after the course is presented and
// the initial course state will be generated using core_course_get_state webservice.
"fullname": "course 3",
"hasprogress": true,
"progress": 10,
- "coursecategory": "Miscellaneous"
+ "coursecategory": "Category 1"
}
]
}
"courseimageurl": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
"fullname": "course 3",
"isfavourite": true,
- "coursecategory": "Miscellaneous"
+ "coursecategory": "Category 1"
}
]
}
// Ensure you are on course management page.
$this->execute("behat_course::i_should_see_the_courses_management_page", get_string('categories'));
- // Select Miscellaneous category.
- $this->i_click_on_category_in_the_management_interface(get_string('miscellaneous'));
+ // Select default course category.
+ $this->i_click_on_category_in_the_management_interface(get_string('defaultcategoryname'));
$this->execute("behat_course::i_should_see_the_courses_management_page", get_string('categoriesandcourses'));
// Click create new course.
And "What to do" "select" should not exist
And "Move into" "select" should exist
And the "Move into" select box should contain "Cat 2"
- And the "Move into" select box should contain "Miscellaneous"
+ And the "Move into" select box should contain "Category 1"
And I press "Cancel"
@javascript
And "What to do" "select" should exist
And I expand the "Move into" autocomplete
And "Cat 2" "autocomplete_suggestions" should not exist
- And "Miscellaneous" "autocomplete_selection" should be visible
+ And "Category 1" "autocomplete_selection" should be visible
And I set the field "What to do" to "Delete all - cannot be undone"
And "Move into" "select" should not be visible
And I press "Cancel"
And I start watching to see if a new page loads
And I should see the "Course categories and courses" management page
And I should see "Course categories" in the "#category-listing h3" "css_element"
- And I should see "Miscellaneous" in the "#course-listing h3" "css_element"
+ And I should see "Category 1" in the "#course-listing h3" "css_element"
And I should see "Cat 1" in the "#category-listing" "css_element"
And I should see "No courses in this category" in the "#course-listing" "css_element"
And I click on category "Cat 1" in the management interface
And I log in as "admin"
And I go to the courses management page
And I should see the "Categories" management page
- And I click on category "Miscellaneous" in the management interface
+ And I click on category "Category 1" in the management interface
And I should see the "Course categories and courses" management page
And I click on "Create new course" "link" in the "#course-listing" "css_element"
When I set the following fields to these values:
And I log in as "user2"
And I am on course index
And I press "Courses pending approval"
- And I should see "Miscellaneous" in the "My new course" "table_row"
+ And I should see "Category 1" in the "My new course" "table_row"
And I click on "Approve" "button" in the "My new course" "table_row"
And I press "Save and return"
And I should see "There are no courses pending approval"
Background:
Given the following "categories" exist:
| name | category | idnumber |
- | Category 1 | 0 | CAT1 |
- | Category 2 | 0 | CAT2 |
- | Category 1 child | CAT1 | CAT11 |
- | Category 2 child | CAT2 | CAT21 |
- | Category 1 child child | CAT11 | CAT111 |
- | Category 3 | 0 | CAT3 |
+ | Category A | 0 | CATA |
+ | Category B | 0 | CATB |
+ | Category A child | CATA | CATA1 |
+ | Category B child | CATB | CATB1 |
+ | Category A child child | CATA1 | CATA11 |
+ | Category C | 0 | CATC |
And the following "courses" exist:
| fullname | shortname | category |
- | Course 1 1 | COURSE1_1 | CAT1 |
- | Course 2 1 | COURSE2_1 | CAT2 |
- | Course 11 1 | COURSE11_1 | CAT11 |
- | Course 2 2 | COURSE2_2 | CAT2 |
- | Course 21 1 | COURSE21_1 | CAT21 |
- | Course 111 1 | COURSE111_1 | CAT111 |
- | Course 111 2 | COURSE111_2 | CAT111 |
+ | Course 1 1 | COURSE1_1 | CATA |
+ | Course 2 1 | COURSE2_1 | CATB |
+ | Course 11 1 | COURSE11_1 | CATA1 |
+ | Course 2 2 | COURSE2_2 | CATB |
+ | Course 21 1 | COURSE21_1 | CATB1 |
+ | Course 111 1 | COURSE111_1 | CATA11 |
+ | Course 111 2 | COURSE111_2 | CATA11 |
And I log in as "admin"
@javascript
| Front page items when logged in | List of categories |
| Maximum category depth | 2 |
And I am on site homepage
- Then I should see "Category 1" in the "region-main" "region"
- And I should see "Category 1 child" in the "region-main" "region"
- And I should not see "Category 1 child child" in the "region-main" "region"
- And I toggle "Category 1" category children visibility in frontpage
- And I should not see "Category 1 child" in the "region-main" "region"
- And I toggle "Category 1" category children visibility in frontpage
- And I should see "Category 1 child" in the "region-main" "region"
- And I toggle "Category 1 child" category children visibility in frontpage
- And I should see "Category 1 child child" in the "region-main" "region"
+ Then I should see "Category A" in the "region-main" "region"
+ And I should see "Category A child" in the "region-main" "region"
+ And I should not see "Category A child child" in the "region-main" "region"
+ And I toggle "Category A" category children visibility in frontpage
+ And I should not see "Category A child" in the "region-main" "region"
+ And I toggle "Category A" category children visibility in frontpage
+ And I should see "Category A child" in the "region-main" "region"
+ And I toggle "Category A child" category children visibility in frontpage
+ And I should see "Category A child child" in the "region-main" "region"
@javascript
Scenario: Displays a combo list
| Front page items when logged in | Combo list |
| Maximum category depth | 2 |
And I am on site homepage
- Then I should see "Category 1" in the "region-main" "region"
- And I should see "Category 1 child" in the "region-main" "region"
- And I should not see "Category 1 child child" in the "region-main" "region"
+ Then I should see "Category A" in the "region-main" "region"
+ And I should see "Category A child" in the "region-main" "region"
+ And I should not see "Category A child child" in the "region-main" "region"
And I should see "Course 1 1" in the "region-main" "region"
And I should see "Course 2 2" in the "region-main" "region"
And I should not see "Course 11 1" in the "region-main" "region"
- And I toggle "Category 1 child" category children visibility in frontpage
+ And I toggle "Category A child" category children visibility in frontpage
And I should see "Course 11 1" in the "region-main" "region"
- And I should see "Category 1 child child" in the "region-main" "region"
- And I toggle "Category 1" category children visibility in frontpage
+ And I should see "Category A child child" in the "region-main" "region"
+ And I toggle "Category A" category children visibility in frontpage
And I should not see "Course 1 1" in the "region-main" "region"
- And I should not see "Category 1 child" in the "region-main" "region"
- And I toggle "Category 1" category children visibility in frontpage
+ And I should not see "Category A child" in the "region-main" "region"
+ And I toggle "Category A" category children visibility in frontpage
And I should see "Course 11 1" in the "region-main" "region"
Scenario: A user can return to the category page from enrolment page
When I log in as "user2"
And I am on course index
- And I follow "Miscellaneous"
+ And I follow "Category 1"
And I follow "Sample course"
And I press "Continue"
Then I should see "Courses" in the ".breadcrumb" "css_element"
And I log out
When I log in as "user1"
And I am on course index
- And I follow "Miscellaneous"
+ And I follow "Category 1"
And I follow "Sample course"
And I follow "Test choice"
And I should see "Sorry, only enrolled users are allowed to make choices."
// $course4
// structure.
- // Note that we also have default 'Miscellaneous' category and default 'site' course.
+ // Note that we also have default course category and default 'site' course.
$this->assertEquals(1, $DB->get_field_sql('SELECT count(*) FROM {course_categories} WHERE id > ?', array($initialcatid)));
$this->assertEquals($category1->id, $DB->get_field_sql('SELECT max(id) FROM {course_categories}'));
$this->assertEquals(1, $DB->get_field_sql('SELECT count(*) FROM {course} WHERE id <> ?', array(SITEID)));
When I log in as "admin"
And I go to the courses management page
And I should see the "Categories" management page
- And I click on category "Miscellaneous" in the management interface
+ And I click on category "Category 1" in the management interface
And I should see the "Course categories and courses" management page
And I click on "Create new course" "link" in the "#course-listing" "css_element"
And I set the following fields to these values:
require_login($course);
$canenrol = has_capability('enrol/manual:enrol', $context);
$canunenrol = has_capability('enrol/manual:unenrol', $context);
-$viewfullnames = has_capability('moodle/site:viewfullnames', $context);
// Note: manage capability not used here because it is used for editing
// of existing enrolments which is not possible here.
$options = array('enrolid' => $enrolid, 'accesscontext' => $context);
$potentialuserselector = new enrol_manual_potential_participant('addselect', $options);
-$potentialuserselector->viewfullnames = $viewfullnames;
$currentuserselector = new enrol_manual_current_participant('removeselect', $options);
-$currentuserselector->viewfullnames = $viewfullnames;
// Build the list of options for the enrolment period dropdown.
$unlimitedperiod = get_string('unlimited');
--- /dev/null
+@enrol @enrol_manual
+Feature: A teacher can manage manually enrolled users in their course
+ In order to manage manually enrolled students in my course
+ As a teacher
+ I can manually add and remove users in my course
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | middlename | lastname | email |
+ | teacher | Teacher | | User | teacher@example.com |
+ | user1 | First | Alice | User | first@example.com |
+ | user2 | Second | Bob | User | second@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher | C1 | editingteacher |
+
+ @javascript
+ Scenario Outline: Manually enrolling users should observe alternative fullname format
+ Given the following config values are set as admin:
+ | alternativefullnameformat | firstname middlename lastname |
+ And the following "permission overrides" exist:
+ | capability | permission | role | contextlevel | reference |
+ | moodle/site:viewfullnames | <permission> | editingteacher | Course | C1 |
+ When I log in as "teacher"
+ And I am on "Course 1" course homepage
+ And I navigate to "Users > Enrolment methods" in current page administration
+ And I click on "Enrol users" "link" in the "Manual enrolments" "table_row"
+ And I set the field "addselect_searchtext" to "First"
+ And I wait "1" seconds
+ And I set the field "Not enrolled users" to "<expectedfullname> (first@example.com)"
+ And I press "Add"
+ Then the "Enrolled users" select box should contain "<expectedfullname> (first@example.com)"
+ Examples:
+ | permission | expectedfullname |
+ | Allow | First Alice User |
+ | Prohibit | First User |
+
+ @javascript
+ Scenario Outline: Manually unenrolling users should observe alternative fullname format
+ Given the following config values are set as admin:
+ | alternativefullnameformat | firstname middlename lastname |
+ And the following "permission overrides" exist:
+ | capability | permission | role | contextlevel | reference |
+ | moodle/site:viewfullnames | <permission> | editingteacher | Course | C1 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | user1 | C1 | student |
+ | user2 | C1 | student |
+ When I log in as "teacher"
+ And I am on "Course 1" course homepage
+ And I navigate to "Users > Enrolment methods" in current page administration
+ And I click on "Enrol users" "link" in the "Manual enrolments" "table_row"
+ And I set the field "removeselect_searchtext" to "First"
+ And I wait "1" seconds
+ And I set the field "Enrolled users" to "<expectedfullname> (first@example.com)"
+ And I press "Remove"
+ Then the "Not enrolled users" select box should contain "<expectedfullname> (first@example.com)"
+ Examples:
+ | permission | expectedfullname |
+ | Allow | First Alice User |
+ | Prohibit | First User |
'filearea' => null,
'itemid' => null,
'filepath' => null,
- 'filename' => 'Miscellaneous');
+ 'filename' => get_string('defaultcategoryname'));
$testdata['parents']['2'] = array('contextid' => $coursecontext->id,
'component' => null,
'filearea' => null,
* @param int $fontsize the font size
* @return string the latex document
*/
- function construct_latex_document( $formula, $fontsize=12 ) {
- global $CFG;
-
- $formula = filter_tex_sanitize_formula($formula);
-
+ function construct_latex_document($formula, $fontsize = 12) {
// $fontsize don't affects to formula's size. $density can change size
- $doc = "\\documentclass[{$fontsize}pt]{article}\n";
+ $doc = "\\documentclass[{$fontsize}pt]{article}\n";
$doc .= get_config('filter_tex', 'latexpreamble');
$doc .= "\\pagestyle{empty}\n";
$doc .= "\\begin{document}\n";
-//dlnsk $doc .= "$ {$formula} $\n";
- if (preg_match("/^[[:space:]]*\\\\begin\\{(gather|align|alignat|multline).?\\}/i",$formula)) {
+ if (preg_match("/^[[:space:]]*\\\\begin\\{(gather|align|alignat|multline).?\\}/i", $formula)) {
$doc .= "$formula\n";
} else {
$doc .= "$ {$formula} $\n";
}
$doc .= "\\end{document}\n";
+
+ // Sanitize the whole document (rather than just the formula) to make sure no one can bypass sanitization
+ // by using \newcommand in preamble to give an alias to a blocked command.
+ $doc = filter_tex_sanitize_formula($doc);
+
return $doc;
}
$convertformat = 'png';
}
$filename = str_replace(".{$convertformat}", '', $filename);
- $tex = "{$this->temp_dir}/$filename.tex";
+ $tex = "$filename.tex"; // Absolute paths won't work with openin_any = p setting.
$dvi = "{$this->temp_dir}/$filename.dvi";
$ps = "{$this->temp_dir}/$filename.ps";
$img = "{$this->temp_dir}/$filename.{$convertformat}";
+ // Change directory to temp dir so that we can work with relative paths.
+ chdir($this->temp_dir);
+
// turn the latex doc into a .tex file in the temp area
$fh = fopen( $tex, 'w' );
fputs( $fh, $doc );
// run latex on document
$command = "$pathlatex --interaction=nonstopmode --halt-on-error $tex";
- chdir( $this->temp_dir );
+
if ($this->execute($command, $log)) { // It allways False on Windows
// return false;
}
'\noexpand', '\line', '\mathcode', '\item', '\section', '\mbox', '\declarerobustcommand',
];
- return str_ireplace($denylist, 'forbiddenkeyword', $texexp);
+ $allowlist = ['inputenc'];
+
+ // Prepare the denylist for regular expression.
+ $denylist = array_map(function($value){
+ return '/' . preg_quote($value, '/') . '/i';
+ }, $denylist);
+
+ // Prepare the allowlist for regular expression.
+ $allowlist = array_map(function($value){
+ return '/\bforbiddenkeyword_(' . preg_quote($value, '/') . ')\b/i';
+ }, $allowlist);
+
+ // First, mangle all denied words.
+ $texexp = preg_replace_callback($denylist,
+ function($matches) {
+ return 'forbiddenkeyword_' . $matches[0];
+ },
+ $texexp
+ );
+
+ // Then, change back the allowed words.
+ $texexp = preg_replace_callback($allowlist,
+ function($matches) {
+ return $matches[1];
+ },
+ $texexp
+ );
+
+ return $texexp;
}
function filter_tex_get_cmd($pathname, $texexp) {
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tex filter library functions tests
+ *
+ * @package filter_tex
+ * @category test
+ * @copyright 2021 Shamim Rezaie <shamim@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types=1);
+
+namespace filter_tex;
+
+use advanced_testcase;
+
+global $CFG;
+require_once($CFG->dirroot . '/filter/tex/lib.php');
+
+/**
+ * Tex filter library functions tests
+ *
+ * @copyright 2021 Shamim Rezaie <shamim@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lib_test extends advanced_testcase {
+ /**
+ * Data provider for test_filter_tex_sanitize_formula.
+ *
+ * @return array
+ */
+ public function filter_tex_sanitize_formula_provider() : array {
+ return [
+ ['x\ =\ \frac{\sqrt{144}}{2}\ \times\ (y\ +\ 12)', 'x\ =\ \frac{\sqrt{144}}{2}\ \times\ (y\ +\ 12)'],
+ ['\usepackage[latin1]{inputenc}', '\usepackage[latin1]{inputenc}'],
+ ['\newcommand{\A}{\verbatiminput}', '\newforbiddenkeyword_command{\A}{\verbatimforbiddenkeyword_input}'],
+ ];
+ }
+
+ /**
+ * Tests for filter_tex_sanitize_formula() function.
+ *
+ * @dataProvider filter_tex_sanitize_formula_provider
+ * @param $formula The formula to test
+ * @param $expected The sanitized version of the formula we expect to get
+ */
+ public function test_filter_tex_sanitize_formula(string $formula, string $expected) {
+ $this->assertEquals($expected, filter_tex_sanitize_formula($formula));
+ }
+}
$output .= "<p>base filename for expression is '$md5'</p>\n";
// temporary paths
- $tex = "$latex->temp_dir/$md5.tex";
+ $tex = "$md5.tex"; // Absolute paths won't work with openin_any = p setting.
$dvi = "$latex->temp_dir/$md5.dvi";
$ps = "$latex->temp_dir/$md5.ps";
$convertformat = get_config('filter_tex', 'convertformat');
$img = "$latex->temp_dir/$md5.{$convertformat}";
+ // Change directory to temp dir so that we can work with relative paths.
+ chdir($latex->temp_dir);
+
// put the expression as a file into the temp area
$expression = html_entity_decode($expression);
$output .= "<p>Processing TeX expression:</p><pre>$expression</pre>\n";
fputs($fh, $doc);
fclose($fh);
- // cd to temp dir
- chdir($latex->temp_dir);
-
// step 1: latex command
$pathlatex = escapeshellarg($pathlatex);
$cmd = "$pathlatex --interaction=nonstopmode --halt-on-error $tex";
$mform->addElement('header', 'general', get_string('pluginname', 'gradeimport_direct'));
// Data upload from copy/paste.
- $mform->addElement('textarea', 'userdata', 'Data', array('rows' => 10, 'class' => 'gradeimport_data_area'));
+ $mform->addElement('textarea', 'userdata', get_string('importdata', 'core_grades'),
+ array('rows' => 10, 'class' => 'gradeimport_data_area'));
+ $mform->addHelpButton('userdata', 'importdata', 'core_grades');
$mform->addRule('userdata', null, 'required');
$mform->setType('userdata', PARAM_RAW);
var optionEl = document.createElement("option");
optionEl.setAttribute("value", roles[i].users[j].id);
optionEl.title = roles[i].users[j].name;
- optionEl.innerHTML = roles[i].users[j].name;
+ optionEl.innerHTML = Y.Escape.html(roles[i].users[j].name);
optgroupEl.appendChild(optionEl);
}
selectEl.appendChild(optgroupEl);
if ($extrafields) {
$extrafieldsdisplay = [];
foreach ($extrafields as $field) {
- $extrafieldsdisplay[] = s($member->{$field});
+ // No escaping here, handled client side in response to AJAX request.
+ $extrafieldsdisplay[] = $member->{$field};
}
$shortmember->name .= ' (' . implode(', ', $extrafieldsdisplay) . ')';
}
$groupoptions[] = (object) [
'value' => $group->id,
'selected' => $selected,
- 'text' => $groupname
+ 'text' => s($groupname)
];
}
}
{{#members}}
<optgroup label="{{role}}">
{{#rolemembers}}
- <option value="{{value}}">{{{text}}}</option>
+ <option value="{{value}}" title="{{{text}}}">{{{text}}}</option>
{{/rolemembers}}
</optgroup>
{{/members}}
$string['pathshead'] = 'Confirmar caminhos';
$string['pathsrodataroot'] = 'A pasta de dados não tem permissões de escrita.';
$string['pathsroparentdataroot'] = 'A pasta ascendente <b>{$a->parent}</b> não tem permissões de escrita. O programa de instalação não conseguiu criar a pasta <b>{$a->dataroot}</b>.';
-$string['pathssubadmindir'] = 'Alguns servidores Web utilizam a pasta <strong>admin</strong> em URLs especiais de acesso a funcionalidades especiais, como é o caso de painéis de controlo. Algumas situações podem criar conflitos com a localização normal das páginas de administração do Moodle. Estes problemas podem ser resolvidos renomeando a pasta <strong>admin</strong> na instalação do Moodle e indicando aqui o novo nome a utilizar. Por exemplo:<br /><br /><b>moodleadmin</b><br /><br />Esta ação resolverá os problemas de acesso das hiperligações para as funcionalidades de administração do Moodle.';
+$string['pathssubadmindir'] = 'Alguns servidores Web utilizam a pasta <strong>admin</strong> em URLs especiais de acesso a funcionalidades especiais, como é o caso de painéis de controlo. Algumas situações podem criar conflitos com a localização normal das páginas de administração do Moodle. Estes problemas podem ser resolvidos renomeando a pasta <strong>admin</strong> na instalação do Moodle e indicando aqui o novo nome a utilizar. Exemplo:<br /><br /><b>moodleadmin</b><br /><br />Esta ação resolverá os problemas de acesso das hiperligações para as funcionalidades de administração do Moodle.';
$string['pathssubdataroot'] = '<p>Pasta onde o Moodle irá armazenar todo o conteúdo de ficheiros enviados pelos utilizadores.</p>
<p>Esta pasta deve ser legível e gravável pelo utilizador do servidor web (geralmente \'www-data\', \'nobody\', ou \'apache\').</p>
<p>Não deve ser acessível diretamente através da web.</p>
$string['chooselanguagehead'] = 'Välj ett språk';
$string['chooselanguagesub'] = 'Vänligen välj ett språk för installationen. Du kommer att ha möjlighet att välja språk för webbplatsen och användarna på en senare skärm.';
$string['clialreadyconfigured'] = 'Filen <em>config.php</em> finns redan. Använd <code>admin/cli/install_database.php</code> för att installera Moodle på denna server.';
-$string['clialreadyinstalled'] = 'Filen config.php finns redan. Vänligen använd admin/cli/upgrade.php om Du vill uppgradera Din webbplats.';
+$string['clialreadyinstalled'] = 'Filen <code>config.php</code> finns redan. Vänligen använd <code>admin/cli/upgrade.php</code> om du vill uppgradera Moodle på den här webbplatsen.';
$string['cliinstallheader'] = 'Kommandoradsbaserat installationsprogram för Moodle {$a}';
$string['clitablesexist'] = 'Databastabellerna finns redan. CLI-installationen kan inte fortsätta.';
$string['databasehost'] = 'Databasserver';
$string['task_stats:dbreads'] = '{$a} reads';
$string['task_stats:dbwrites'] = '{$a} writes';
$string['task_status'] = 'Task status';
-$string['task_status_desc'] = 'The task <q>{$a->name}</q> is <strong>{$a->status}</strong>.<br />See its <a href="{$a->gotourl}">details</a>.<br />Class: {$a->class}{$a->extradescription}';
+$string['task_status_desc'] = 'The task \'{$a->name}\' is {$a->status}. For details, see {$a->class}{$a->extradescription} in <a href="{$a->gotourl}">Scheduled tasks</a>.';
$string['task_starttime'] = 'Start time';
$string['task_duration'] = 'Duration';
$string['task_dbstats'] = 'Database';
$string['testoutgoingmailconf'] = 'Test outgoing mail configuration';
$string['testoutgoingmaildetail'] = 'Note: Before testing, please save your configuration.<br />{$a}';
$string['testoutgoingmailconf_errorcommunications'] = 'Your site couldn\'t communicate with your mail server. Please check your outgoing mail configuration.';
-$string['testoutgoingmailconf_message'] = "This is a test message to confirm that you have successfully configured your site's outgoing mail.\n\n Sent:" . '{$a}';
+$string['testoutgoingmailconf_message'] = 'This is a test message to confirm that you have successfully configured your site\'s outgoing mail. Sent: {$a}';
$string['testoutgoingmailconf_fromemail'] = 'From username or email address';
$string['testoutgoingmailconf_fromemail_help'] = 'This field emulates sending the message from that user, but the From header used in the real email sent will depend on other settings such as allowedemaildomains';
$string['testoutgoingmailconf_fromemail_invalid'] = 'Invalid From username or email. Must be a valid email format or an existing username in Moodle.';
$string['overwrite'] = 'Overwrite';
$string['pendingasyncdetail'] = 'Asynchronous backups only allow a user to have one pending backup for a resource at a time. <br/> Multiple asynchronous backups of the same resource can\'t be queued, as this would likely result in multiple backups with the same content.';
$string['pendingasyncdeletedetail'] = 'This course has an asynchronous backup pending. <br/> Courses can\'t be deleted until this backup finishes.';
-$string['pendingasyncedit'] = 'There is a pending asynchronous backup for this course. Please do not edit this course until backup is complete.';
+$string['pendingasyncedit'] = 'There is a pending backup or copy requested for this course. Please do not edit the course until this is complete.';
$string['pendingasyncerror'] = 'Backup pending for this resource';
$string['previousstage'] = 'Previous';
$string['preparingui'] = 'Preparing to display page';