GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP => 'REMOTE_ADDR');
$temp->add(new admin_setting_configselect('getremoteaddrconf', new lang_string('getremoteaddrconf', 'admin'),
new lang_string('configgetremoteaddrconf', 'admin'),
- GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP, $options));
+ GETREMOTEADDR_SKIP_DEFAULT, $options));
$temp->add(new admin_setting_configtext('reverseproxyignore', new lang_string('reverseproxyignore', 'admin'), new lang_string('configreverseproxyignore', 'admin'), ''));
$temp->add(new admin_setting_heading('webproxy', new lang_string('webproxy', 'admin'), new lang_string('webproxyinfo', 'admin')));
$ADMIN->add('mobileapp', $temp);
- // Show only mobile settings if the mobile service is enabled.
- if (!empty($CFG->enablemobilewebservice)) {
+ $featuresnotice = null;
+ if (empty($CFG->disablemobileappsubscription)) {
+ // General notification about limited features due to app restrictions.
+ $subscriptionurl = (new moodle_url("/$CFG->admin/tool/mobile/subscription.php"))->out(false);
+ $notify = new \core\output\notification(
+ get_string('moodleappsportalfeatureswarning', 'tool_mobile', $subscriptionurl),
+ \core\output\notification::NOTIFY_WARNING);
+ $featuresnotice = $OUTPUT->render($notify);
+ }
+
+ $hideappsubscription = empty($CFG->enablemobilewebservice);
+ $hideappsubscription = $hideappsubscription || (isset($CFG->disablemobileappsubscription) && !empty($CFG->disablemobileappsubscription));
+
+ $ADMIN->add(
+ 'mobileapp',
+ new admin_externalpage(
+ 'mobileappsubscription',
+ new lang_string('mobileappsubscription', 'tool_mobile'),
+ "$CFG->wwwroot/$CFG->admin/tool/mobile/subscription.php",
+ 'moodle/site:config',
+ $hideappsubscription
+ )
+ );
+
+ // Type of login.
+ $temp = new admin_settingpage(
+ 'mobileauthentication',
+ new lang_string('mobileauthentication', 'tool_mobile'),
+ 'moodle/site:config',
+ empty($CFG->enablemobilewebservice)
+ );
+
+ $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesauth', '', $featuresnotice));
+
+ $options = array(
+ tool_mobile\api::LOGIN_VIA_APP => new lang_string('loginintheapp', 'tool_mobile'),
+ tool_mobile\api::LOGIN_VIA_BROWSER => new lang_string('logininthebrowser', 'tool_mobile'),
+ tool_mobile\api::LOGIN_VIA_EMBEDDED_BROWSER => new lang_string('loginintheembeddedbrowser', 'tool_mobile'),
+ );
+ $temp->add(new admin_setting_configselect('tool_mobile/typeoflogin',
+ new lang_string('typeoflogin', 'tool_mobile'),
+ new lang_string('typeoflogin_desc', 'tool_mobile'), 1, $options));
+
+ $options = [
+ tool_mobile\api::QR_CODE_DISABLED => new lang_string('qrcodedisabled', 'tool_mobile'),
+ tool_mobile\api::QR_CODE_URL => new lang_string('qrcodetypeurl', 'tool_mobile'),
+ tool_mobile\api::QR_CODE_LOGIN => new lang_string('qrcodetypelogin', 'tool_mobile'),
+ ];
+ $temp->add(new admin_setting_configselect('tool_mobile/qrcodetype',
+ new lang_string('qrcodetype', 'tool_mobile'),
+ new lang_string('qrcodetype_desc', 'tool_mobile'), tool_mobile\api::QR_CODE_LOGIN, $options));
+
+ $temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
+ new lang_string('forcedurlscheme_key', 'tool_mobile'),
+ new lang_string('forcedurlscheme', 'tool_mobile'), 'moodlemobile', PARAM_NOTAGS));
+
+ $temp->add(new admin_setting_configtext('tool_mobile/minimumversion',
+ new lang_string('minimumversion_key', 'tool_mobile'),
+ new lang_string('minimumversion', 'tool_mobile'), '', PARAM_NOTAGS));
+
+ $ADMIN->add('mobileapp', $temp);
+
+ // Appearance related settings.
+ $temp = new admin_settingpage(
+ 'mobileappearance',
+ new lang_string('mobileappearance', 'tool_mobile'),
+ 'moodle/site:config',
+ empty($CFG->enablemobilewebservice)
+ );
+
+ if (!empty($featuresnotice)) {
+ $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesappearance', '', $featuresnotice));
+ }
+
+ $temp->add(new admin_setting_configtext('mobilecssurl', new lang_string('mobilecssurl', 'tool_mobile'),
+ new lang_string('configmobilecssurl', 'tool_mobile'), '', PARAM_URL));
+
+ // Reference to Branded Mobile App.
+ if (empty($CFG->disableserviceads_branded)) {
+ $temp->add(new admin_setting_description('moodlebrandedappreference',
+ new lang_string('moodlebrandedapp', 'admin'),
+ new lang_string('moodlebrandedappreference', 'admin')
+ ));
+ }
+
+ $temp->add(new admin_setting_heading('tool_mobile/smartappbanners',
+ new lang_string('smartappbanners', 'tool_mobile'), ''));
- $featuresnotice = null;
- if (empty($CFG->disablemobileappsubscription)) {
- // General notification about limited features due to app restrictions.
- $subscriptionurl = (new moodle_url("/$CFG->admin/tool/mobile/subscription.php"))->out(false);
- $notify = new \core\output\notification(
- get_string('moodleappsportalfeatureswarning', 'tool_mobile', $subscriptionurl),
- \core\output\notification::NOTIFY_WARNING);
- $featuresnotice = $OUTPUT->render($notify);
-
- $ADMIN->add('mobileapp', new admin_externalpage('mobileappsubscription',
- new lang_string('mobileappsubscription', 'tool_mobile'),
- "$CFG->wwwroot/$CFG->admin/tool/mobile/subscription.php"));
- }
-
- // Type of login.
- $temp = new admin_settingpage('mobileauthentication', new lang_string('mobileauthentication', 'tool_mobile'));
-
- $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesauth', '', $featuresnotice));
-
- $options = array(
- tool_mobile\api::LOGIN_VIA_APP => new lang_string('loginintheapp', 'tool_mobile'),
- tool_mobile\api::LOGIN_VIA_BROWSER => new lang_string('logininthebrowser', 'tool_mobile'),
- tool_mobile\api::LOGIN_VIA_EMBEDDED_BROWSER => new lang_string('loginintheembeddedbrowser', 'tool_mobile'),
- );
- $temp->add(new admin_setting_configselect('tool_mobile/typeoflogin',
- new lang_string('typeoflogin', 'tool_mobile'),
- new lang_string('typeoflogin_desc', 'tool_mobile'), 1, $options));
-
- $options = [
- tool_mobile\api::QR_CODE_DISABLED => new lang_string('qrcodedisabled', 'tool_mobile'),
- tool_mobile\api::QR_CODE_URL => new lang_string('qrcodetypeurl', 'tool_mobile'),
- tool_mobile\api::QR_CODE_LOGIN => new lang_string('qrcodetypelogin', 'tool_mobile'),
- ];
- $temp->add(new admin_setting_configselect('tool_mobile/qrcodetype',
- new lang_string('qrcodetype', 'tool_mobile'),
- new lang_string('qrcodetype_desc', 'tool_mobile'), tool_mobile\api::QR_CODE_LOGIN, $options));
-
- $temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
- new lang_string('forcedurlscheme_key', 'tool_mobile'),
- new lang_string('forcedurlscheme', 'tool_mobile'), 'moodlemobile', PARAM_NOTAGS));
-
- $temp->add(new admin_setting_configtext('tool_mobile/minimumversion',
- new lang_string('minimumversion_key', 'tool_mobile'),
- new lang_string('minimumversion', 'tool_mobile'), '', PARAM_NOTAGS));
-
- $ADMIN->add('mobileapp', $temp);
-
- // Appearance related settings.
- $temp = new admin_settingpage('mobileappearance', new lang_string('mobileappearance', 'tool_mobile'));
-
- if (!empty($featuresnotice)) {
- $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesappearance', '', $featuresnotice));
- }
-
- $temp->add(new admin_setting_configtext('mobilecssurl', new lang_string('mobilecssurl', 'tool_mobile'),
- new lang_string('configmobilecssurl', 'tool_mobile'), '', PARAM_URL));
-
- // Reference to Branded Mobile App.
- if (empty($CFG->disableserviceads_branded)) {
- $temp->add(new admin_setting_description('moodlebrandedappreference',
- new lang_string('moodlebrandedapp', 'admin'),
- new lang_string('moodlebrandedappreference', 'admin')
- ));
- }
-
- $temp->add(new admin_setting_heading('tool_mobile/smartappbanners',
- new lang_string('smartappbanners', 'tool_mobile'), ''));
-
- $temp->add(new admin_setting_configcheckbox('tool_mobile/enablesmartappbanners',
- new lang_string('enablesmartappbanners', 'tool_mobile'),
- new lang_string('enablesmartappbanners_desc', 'tool_mobile'), 0));
-
- $temp->add(new admin_setting_configtext('tool_mobile/iosappid', new lang_string('iosappid', 'tool_mobile'),
- new lang_string('iosappid_desc', 'tool_mobile'), tool_mobile\api::DEFAULT_IOS_APP_ID, PARAM_ALPHANUM));
-
- $temp->add(new admin_setting_configtext('tool_mobile/androidappid', new lang_string('androidappid', 'tool_mobile'),
- new lang_string('androidappid_desc', 'tool_mobile'), tool_mobile\api::DEFAULT_ANDROID_APP_ID, PARAM_NOTAGS));
-
- $temp->add(new admin_setting_configtext('tool_mobile/setuplink', new lang_string('setuplink', 'tool_mobile'),
- new lang_string('setuplink_desc', 'tool_mobile'), 'https://download.moodle.org/mobile', PARAM_URL));
-
- $ADMIN->add('mobileapp', $temp);
-
- // Features related settings.
- $temp = new admin_settingpage('mobilefeatures', new lang_string('mobilefeatures', 'tool_mobile'));
-
- if (!empty($featuresnotice)) {
- $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeatures', '', $featuresnotice));
- }
-
- $temp->add(new admin_setting_heading('tool_mobile/logout',
- new lang_string('logout'), ''));
-
- $temp->add(new admin_setting_configcheckbox('tool_mobile/forcelogout',
- new lang_string('forcelogout', 'tool_mobile'),
- new lang_string('forcelogout_desc', 'tool_mobile'), 0));
-
- $temp->add(new admin_setting_heading('tool_mobile/features',
- new lang_string('mobilefeatures', 'tool_mobile'), ''));
+ $temp->add(new admin_setting_configcheckbox('tool_mobile/enablesmartappbanners',
+ new lang_string('enablesmartappbanners', 'tool_mobile'),
+ new lang_string('enablesmartappbanners_desc', 'tool_mobile'), 0));
- $options = tool_mobile\api::get_features_list();
- $temp->add(new admin_setting_configmultiselect('tool_mobile/disabledfeatures',
- new lang_string('disabledfeatures', 'tool_mobile'),
- new lang_string('disabledfeatures_desc', 'tool_mobile'), array(), $options));
+ $temp->add(new admin_setting_configtext('tool_mobile/iosappid', new lang_string('iosappid', 'tool_mobile'),
+ new lang_string('iosappid_desc', 'tool_mobile'), tool_mobile\api::DEFAULT_IOS_APP_ID, PARAM_ALPHANUM));
- $temp->add(new admin_setting_configtextarea('tool_mobile/custommenuitems',
- new lang_string('custommenuitems', 'tool_mobile'),
- new lang_string('custommenuitems_desc', 'tool_mobile'), '', PARAM_RAW, '50', '10'));
+ $temp->add(new admin_setting_configtext('tool_mobile/androidappid', new lang_string('androidappid', 'tool_mobile'),
+ new lang_string('androidappid_desc', 'tool_mobile'), tool_mobile\api::DEFAULT_ANDROID_APP_ID, PARAM_NOTAGS));
- $temp->add(new admin_setting_heading('tool_mobile/language',
- new lang_string('language'), ''));
+ $temp->add(new admin_setting_configtext('tool_mobile/setuplink', new lang_string('setuplink', 'tool_mobile'),
+ new lang_string('setuplink_desc', 'tool_mobile'), 'https://download.moodle.org/mobile', PARAM_URL));
- $temp->add(new admin_setting_configtextarea('tool_mobile/customlangstrings',
- new lang_string('customlangstrings', 'tool_mobile'),
- new lang_string('customlangstrings_desc', 'tool_mobile'), '', PARAM_RAW, '50', '10'));
+ $ADMIN->add('mobileapp', $temp);
+
+ // Features related settings.
+ $temp = new admin_settingpage(
+ 'mobilefeatures',
+ new lang_string('mobilefeatures', 'tool_mobile'),
+ 'moodle/site:config',
+ empty($CFG->enablemobilewebservice)
+ );
- $ADMIN->add('mobileapp', $temp);
+ if (!empty($featuresnotice)) {
+ $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeatures', '', $featuresnotice));
}
+
+ $temp->add(new admin_setting_heading('tool_mobile/logout',
+ new lang_string('logout'), ''));
+
+ $temp->add(new admin_setting_configcheckbox('tool_mobile/forcelogout',
+ new lang_string('forcelogout', 'tool_mobile'),
+ new lang_string('forcelogout_desc', 'tool_mobile'), 0));
+
+ $temp->add(new admin_setting_heading('tool_mobile/features',
+ new lang_string('mobilefeatures', 'tool_mobile'), ''));
+
+ $options = tool_mobile\api::get_features_list();
+ $temp->add(new admin_setting_configmultiselect('tool_mobile/disabledfeatures',
+ new lang_string('disabledfeatures', 'tool_mobile'),
+ new lang_string('disabledfeatures_desc', 'tool_mobile'), array(), $options));
+
+ $temp->add(new admin_setting_configtextarea('tool_mobile/custommenuitems',
+ new lang_string('custommenuitems', 'tool_mobile'),
+ new lang_string('custommenuitems_desc', 'tool_mobile'), '', PARAM_RAW, '50', '10'));
+
+ $temp->add(new admin_setting_heading('tool_mobile/language',
+ new lang_string('language'), ''));
+
+ $temp->add(new admin_setting_configtextarea('tool_mobile/customlangstrings',
+ new lang_string('customlangstrings', 'tool_mobile'),
+ new lang_string('customlangstrings_desc', 'tool_mobile'), '', PARAM_RAW, '50', '10'));
+
+ $ADMIN->add('mobileapp', $temp);
}
// Get list of module types on course.
$modinfo = get_fast_modinfo($COURSE);
- $modnames = $modinfo->get_used_module_names(true);
+ $modnames = array_map('strval', $modinfo->get_used_module_names(true));
+ core_collator::asort($modnames);
$PAGE->requires->yui_module('moodle-backup-backupselectall', 'M.core_backup.backupselectall',
array($modnames));
$PAGE->requires->strings_for_js(array('select', 'all', 'none'), 'moodle');
| fullname | shortname |
| Course 1 | C1 |
| Course 2 | C2 |
- And the following "activities" exist:
- | activity | name | intro | course | idnumber | globalglossary | defaultapproval |
- | glossary | Tips and Tricks | Frontpage glossary description | C2 | glossary0 | 1 | 1 |
+ And the following "activity" exists:
+ | activity | glossary |
+ | name | Tips and Tricks |
+ | intro | Frontpage glossary description |
+ | course | C2 |
+ | idnumber | glossary0 |
+ | globalglossary | 1 |
+ | defaultapproval | 1 |
And the following "users" exist:
| username | firstname | lastname | email |
| student1 | Sam1 | Student1 | student1@example.com |
--- /dev/null
+@core @core_completion
+Feature: Set completion of other courses as criteria for completion of current course
+ In order to set completion of other courses as criteria for completion of current course
+ As a user
+ I want to select the prerequisite courses in completion settings
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname | category | enablecompletion |
+ | Course 1 | C1 | 0 | 1 |
+ | Course 2 | C2 | 0 | 1 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | One | student1@example.com |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | student1 | C1 | student |
+
+ @javascript
+ Scenario: Set completion of prerequisite course as completion criteria of current course
+ When I log in as "admin"
+ And I am on "Course 1" course homepage with editing mode on
+ And I navigate to "Course completion" in current page administration
+ And I click on "Condition: Completion of other courses" "link"
+ And I set the field "Courses available" to "Course 2"
+ And I press "Save changes"
+ And I add the "Course completion status" block
+ And I click on "View course report" "link" in the "Course completion status" "block"
+ Then I should see "Course 2" in the "completion-progress" "table"
+ And I should see "Student One" in the "completion-progress" "table"
if ($category->can_change_sortorder()) {
$actions['moveup'] = array(
'url' => new \moodle_url($baseurl, array('action' => 'movecategoryup')),
- 'icon' => new \pix_icon('t/up', new \lang_string('up')),
- 'string' => new \lang_string('up')
+ 'icon' => new \pix_icon('t/up', new \lang_string('moveup')),
+ 'string' => new \lang_string('moveup')
);
$actions['movedown'] = array(
'url' => new \moodle_url($baseurl, array('action' => 'movecategorydown')),
- 'icon' => new \pix_icon('t/down', new \lang_string('down')),
- 'string' => new \lang_string('down')
+ 'icon' => new \pix_icon('t/down', new \lang_string('movedown')),
+ 'string' => new \lang_string('movedown')
);
}
*
* @param \core_course_category $category
* @param \core_course_list_element $course
- * @return string
+ * @return array
*/
public static function get_course_listitem_actions(\core_course_category $category, \core_course_list_element $course) {
$baseurl = new \moodle_url(
if ($category->can_resort_courses()) {
$actions[] = array(
'url' => new \moodle_url($baseurl, array('action' => 'movecourseup')),
- 'icon' => new \pix_icon('t/up', \get_string('up')),
+ 'icon' => new \pix_icon('t/up', \get_string('moveup')),
'attributes' => array('data-action' => 'moveup', 'class' => 'action-moveup')
);
$actions[] = array(
'url' => new \moodle_url($baseurl, array('action' => 'movecoursedown')),
- 'icon' => new \pix_icon('t/down', \get_string('down')),
+ 'icon' => new \pix_icon('t/down', \get_string('movedown')),
'attributes' => array('data-action' => 'movedown', 'class' => 'action-movedown')
);
}
$categoryid = '';
}
$select = new single_select($this->page->url, 'categoryid', $categories, $categoryid, $nothing);
+ $select->attributes['aria-label'] = get_string('selectacategory');
$html .= $this->render($select);
}
$html .= html_writer::end_div();
$html .= html_writer::start_div('float-left ' . $checkboxclass);
$html .= html_writer::start_div('custom-control custom-checkbox mr-1 ');
$html .= html_writer::empty_tag('input', $bcatinput);
- $html .= html_writer::tag('label', '', array(
- 'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+ $labeltext = html_writer::span(get_string('bulkactionselect', 'moodle', $text), 'sr-only');
+ $html .= html_writer::tag('label', $labeltext, array(
'class' => 'custom-control-label',
'for' => 'categorylistitem' . $category->id));
$html .= html_writer::end_div();
$html .= html_writer::start_div('card-body');
$html .= $this->course_listing_actions($category, $course, $perpage);
$html .= $this->listing_pagination($category, $page, $perpage, false, $viewmode);
- $html .= html_writer::start_tag('ul', array('class' => 'ml course-list', 'role' => 'group'));
+ $html .= html_writer::start_tag('ul', array('class' => 'ml course-list'));
foreach ($category->get_courses($options) as $listitem) {
$html .= $this->course_listitem($category, $listitem, $courseid);
}
$html .= html_writer::start_div('float-left ' . $checkboxclass);
$html .= html_writer::start_div('custom-control custom-checkbox mr-1 ');
$html .= html_writer::empty_tag('input', $bulkcourseinput);
- $html .= html_writer::tag('label', '', array(
- 'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+ $labeltext = html_writer::span(get_string('bulkactionselect', 'moodle', $text), 'sr-only');
+ $html .= html_writer::tag('label', $labeltext, array(
'class' => 'custom-control-label',
'for' => 'courselistitem' . $course->id));
$html .= html_writer::end_div();
if ($bulkcourseinput) {
$html .= html_writer::start_div('custom-control custom-checkbox mr-1');
$html .= html_writer::empty_tag('input', $bulkcourseinput);
- $html .= html_writer::tag('label', '', array(
- 'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+ $labeltext = html_writer::span(get_string('bulkactionselect', 'moodle', $text), 'sr-only');
+ $html .= html_writer::tag('label', $labeltext, array(
'class' => 'custom-control-label',
'for' => 'coursesearchlistitem' . $course->id));
$html .= html_writer::end_div();
$output .= html_writer::start_tag('form', array('class' => 'card', 'id' => $formid,
'action' => $searchurl, 'method' => 'get'));
$output .= html_writer::start_tag('fieldset', array('class' => 'coursesearchbox invisiblefieldset'));
- $output .= html_writer::tag('div', $this->output->heading($strsearchcourses.': ', 2, 'm-0'),
+ $output .= html_writer::tag('legend', $this->output->heading($strsearchcourses.': ', 2, 'm-0'),
array('class' => 'card-header'));
$output .= html_writer::start_div('card-body');
$output .= html_writer::start_div('input-group col-sm-6 col-lg-4 m-auto');
$output .= html_writer::empty_tag('input', array('class' => 'form-control', 'type' => 'text', 'id' => $inputid,
- 'size' => $inputsize, 'name' => 'search', 'value' => s($value)));
+ 'size' => $inputsize, 'name' => 'search', 'value' => s($value), 'aria-label' => get_string('searchcourses')));
$output .= html_writer::start_tag('span', array('class' => 'input-group-btn'));
$output .= html_writer::tag('button', get_string('go'), array('class' => 'btn btn-primary', 'type' => 'submit'));
$output .= html_writer::end_tag('span');
}
// Get applicable courses (prerequisites).
- $selectedcourses = $DB->get_fieldset_sql("SELECT cc.courseinstance
- FROM {course_completion_criteria} cc WHERE cc.course = ?", [$course->id]);
$hasselectablecourses = core_course_category::search_courses(['onlywithcompletion' => true], ['limit' => 2]);
unset($hasselectablecourses[$course->id]);
if ($hasselectablecourses) {
// Show multiselect box.
$mform->addElement('course', 'criteria_course', get_string('coursesavailable', 'completion'),
array('multiple' => 'multiple', 'onlywithcompletion' => true, 'exclude' => $course->id));
+ $mform->setType('criteria_course', PARAM_INT);
+
+ $selectedcourses = $DB->get_fieldset_select('course_completion_criteria', 'courseinstance',
+ 'course = :course AND criteriatype = :type', ['course' => $course->id, 'type' => COMPLETION_CRITERIA_TYPE_COURSE]);
$mform->setDefault('criteria_course', $selectedcourses);
// Map aggregation methods to context-sensitive human readable dropdown menu.
}
/**
- * Returns full course categories trees to be used in html_writer::select()
+ * Returns the list of full course categories to be used in html_writer::select()
*
- * Calls {@link core_course_category::make_categories_list()} to build the tree and
- * adds whitespace to denote nesting
+ * Calls {@see core_course_category::make_categories_list()} to build the list.
*
* @return array array mapping course category id to the display name
*/
function make_categories_options() {
- $cats = core_course_category::make_categories_list('', 0, ' / ');
- foreach ($cats as $key => $value) {
- // Prefix the value with the number of spaces equal to category depth (number of separators in the value).
- $cats[$key] = str_repeat(' ', substr_count($value, ' / ')). $value;
- }
- return $cats;
+ return core_course_category::make_categories_list('', 0, ' / ');
}
/**
And I navigate to "Courses > Activity chooser > Recommended activities" in site administration
When I set the field "search" to "assign"
And I click on "Submit search" "button"
- Then I should see "Search results: 1"
+ Then I should see "Search results"
And "Assignment" "table_row" should exist
And "Book" "table_row" should not exist
$matchingcontentitems1 = $cis->get_content_items_by_name_pattern($user, $pattern1);
$matchingcontentitems2 = $cis->get_content_items_by_name_pattern($user, $pattern2);
- // The pattern "assign" should return 1 content item ("Assignment").
- $this->assertCount(1, $matchingcontentitems1);
- $this->assertEquals("Assignment", $matchingcontentitems1[0]->title);
+ // The pattern "assign" should return at least 1 content item (ex. "Assignment").
+ $this->assertGreaterThanOrEqual(1, count($matchingcontentitems1));
+ // Verify the pattern "assign" can be found in the title of each returned content item.
+ foreach ($matchingcontentitems1 as $contentitem) {
+ $this->assertEquals(1, preg_match("/$pattern1/i", $contentitem->title));
+ }
// The pattern "random string" should not return any content items.
$this->assertEmpty($matchingcontentitems2);
}
if (fetchMap === null) {
fetchMap = Ajax.call([{
- methodname: 'core_output_load_fontawesome_icon_map',
- args: []
+ methodname: 'core_output_load_fontawesome_icon_system_map',
+ args: {
+ themename: M.cfg.theme,
+ },
}], true, false, false, 0, M.cfg.themerev)[0];
}
class behat_core_generator extends behat_generator_base {
protected function get_creatable_entities(): array {
- return [
+ $entities = [
'users' => [
+ 'singular' => 'user',
'datagenerator' => 'user',
'required' => ['username'],
],
'categories' => [
+ 'singular' => 'category',
'datagenerator' => 'category',
'required' => ['idnumber'],
'switchids' => ['category' => 'parent'],
],
'courses' => [
+ 'singular' => 'course',
'datagenerator' => 'course',
'required' => ['shortname'],
'switchids' => ['category' => 'category'],
],
'groups' => [
+ 'singular' => 'group',
'datagenerator' => 'group',
'required' => ['idnumber', 'course'],
'switchids' => ['course' => 'courseid'],
],
'groupings' => [
+ 'singular' => 'grouping',
'datagenerator' => 'grouping',
'required' => ['idnumber', 'course'],
'switchids' => ['course' => 'courseid'],
],
'course enrolments' => [
+ 'singular' => 'course enrolment',
'datagenerator' => 'enrol_user',
'required' => ['user', 'course', 'role'],
'switchids' => ['user' => 'userid', 'course' => 'courseid', 'role' => 'roleid'],
],
'custom field categories' => [
+ 'singular' => 'custom field category',
'datagenerator' => 'custom_field_category',
'required' => ['name', 'component', 'area', 'itemid'],
'switchids' => [],
],
'custom fields' => [
+ 'singular' => 'custom field',
'datagenerator' => 'custom_field',
'required' => ['name', 'category', 'type', 'shortname'],
'switchids' => [],
],
'permission overrides' => [
+ 'singular' => 'permission override',
'datagenerator' => 'permission_override',
'required' => ['capability', 'permission', 'role', 'contextlevel', 'reference'],
'switchids' => ['role' => 'roleid'],
],
'system role assigns' => [
+ 'singular' => 'system role assignment',
'datagenerator' => 'system_role_assign',
'required' => ['user', 'role'],
'switchids' => ['user' => 'userid', 'role' => 'roleid'],
],
'role assigns' => [
+ 'singular' => 'role assignment',
'datagenerator' => 'role_assign',
'required' => ['user', 'role', 'contextlevel', 'reference'],
'switchids' => ['user' => 'userid', 'role' => 'roleid'],
],
'activities' => [
+ 'singular' => 'activity',
'datagenerator' => 'activity',
'required' => ['activity', 'idnumber', 'course'],
'switchids' => ['course' => 'course', 'gradecategory' => 'gradecat', 'grouping' => 'groupingid'],
],
'blocks' => [
+ 'singular' => 'block',
'datagenerator' => 'block_instance',
'required' => ['blockname', 'contextlevel', 'reference'],
],
'group members' => [
+ 'singular' => 'group member',
'datagenerator' => 'group_member',
'required' => ['user', 'group'],
'switchids' => ['user' => 'userid', 'group' => 'groupid'],
],
'grouping groups' => [
+ 'singular' => 'grouping group',
'datagenerator' => 'grouping_group',
'required' => ['grouping', 'group'],
'switchids' => ['grouping' => 'groupingid', 'group' => 'groupid'],
],
'cohorts' => [
+ 'singular' => 'cohort',
'datagenerator' => 'cohort',
'required' => ['idnumber'],
],
'cohort members' => [
+ 'singular' => 'cohort member',
'datagenerator' => 'cohort_member',
'required' => ['user', 'cohort'],
'switchids' => ['user' => 'userid', 'cohort' => 'cohortid'],
],
'roles' => [
+ 'singular' => 'role',
'datagenerator' => 'role',
'required' => ['shortname'],
],
'grade categories' => [
+ 'singular' => 'grade category',
'datagenerator' => 'grade_category',
'required' => ['fullname', 'course'],
'switchids' => ['course' => 'courseid', 'gradecategory' => 'parent'],
],
'grade items' => [
+ 'singular' => 'grade item',
'datagenerator' => 'grade_item',
'required' => ['course'],
'switchids' => [
],
],
'grade outcomes' => [
+ 'singular' => 'grade outcome',
'datagenerator' => 'grade_outcome',
'required' => ['shortname', 'scale'],
'switchids' => ['course' => 'courseid', 'gradecategory' => 'categoryid', 'scale' => 'scaleid'],
],
'scales' => [
+ 'singular' => 'scale',
'datagenerator' => 'scale',
'required' => ['name', 'scale'],
'switchids' => ['course' => 'courseid'],
],
'question categories' => [
+ 'singular' => 'question category',
'datagenerator' => 'question_category',
'required' => ['name', 'contextlevel', 'reference'],
'switchids' => ['questioncategory' => 'parent'],
],
'questions' => [
+ 'singular' => 'question',
'datagenerator' => 'question',
'required' => ['qtype', 'questioncategory', 'name'],
'switchids' => ['questioncategory' => 'category', 'user' => 'createdby'],
],
'tags' => [
+ 'singular' => 'tag',
'datagenerator' => 'tag',
'required' => ['name'],
],
'events' => [
+ 'singular' => 'event',
'datagenerator' => 'event',
'required' => ['name', 'eventtype'],
'switchids' => [
],
],
'message contacts' => [
+ 'singular' => 'message contact',
'datagenerator' => 'message_contacts',
'required' => ['user', 'contact'],
'switchids' => ['user' => 'userid', 'contact' => 'contactid'],
],
'private messages' => [
+ 'singular' => 'private message',
'datagenerator' => 'private_messages',
'required' => ['user', 'contact', 'message'],
'switchids' => ['user' => 'userid', 'contact' => 'contactid'],
],
'favourite conversations' => [
+ 'singular' => 'favourite conversation',
'datagenerator' => 'favourite_conversations',
'required' => ['user', 'contact'],
'switchids' => ['user' => 'userid', 'contact' => 'contactid'],
],
'group messages' => [
+ 'singular' => 'group message',
'datagenerator' => 'group_messages',
'required' => ['user', 'group', 'message'],
'switchids' => ['user' => 'userid', 'group' => 'groupid'],
],
'muted group conversations' => [
+ 'singular' => 'muted group conversation',
'datagenerator' => 'mute_group_conversations',
'required' => ['user', 'group', 'course'],
'switchids' => ['user' => 'userid', 'group' => 'groupid', 'course' => 'courseid'],
],
'muted private conversations' => [
+ 'singular' => 'muted private conversation',
'datagenerator' => 'mute_private_conversations',
'required' => ['user', 'contact'],
'switchids' => ['user' => 'userid', 'contact' => 'contactid'],
],
'language customisations' => [
+ 'singular' => 'language customisation',
'datagenerator' => 'customlang',
'required' => ['component', 'stringid', 'value'],
],
- 'analytics model' => [
+ 'analytics models' => [
+ 'singular' => 'analytics model',
'datagenerator' => 'analytics_model',
'required' => ['target', 'indicators', 'timesplitting', 'enabled'],
],
'user preferences' => [
+ 'singular' => 'user preference',
'datagenerator' => 'user_preferences',
'required' => array('user', 'preference', 'value'),
- 'switchids' => array('user' => 'userid')
+ 'switchids' => array('user' => 'userid'),
],
- 'contentbank content' => [
+ 'contentbank contents' => [
+ 'singular' => 'contentbank content',
'datagenerator' => 'contentbank_content',
'required' => array('contextlevel', 'reference', 'contenttype', 'user', 'contentname'),
'switchids' => array('user' => 'userid')
],
- 'badge external backpack' => [
+ 'badge external backpacks' => [
+ 'singular' => 'badge external backpack',
'datagenerator' => 'badge_external_backpack',
'required' => ['backpackapiurl', 'backpackweburl', 'apiversion']
],
- 'setup backpack connected' => [
+ 'setup backpacks connected' => [
+ 'singular' => 'setup backpack connected',
'datagenerator' => 'setup_backpack_connected',
'required' => ['user', 'externalbackpack'],
'switchids' => ['user' => 'userid', 'externalbackpack' => 'externalbackpackid']
],
'last access times' => [
+ 'singular' => 'last access time',
'datagenerator' => 'last_access_times',
'required' => ['user', 'course', 'lastaccess'],
'switchids' => ['user' => 'userid', 'course' => 'courseid'],
],
];
+
+ return $entities;
}
/**
*
* @param string $generatortype The name of the entity to create.
* @param TableNode $data from the step.
+ * @param bool $singular Whether there is only one record and it is pivotted
*/
- public function generate_items(string $generatortype, TableNode $data) {
+ public function generate_items(string $generatortype, TableNode $data, bool $singular = false) {
// Now that we need them require the data generators.
require_once(__DIR__ . '/../../testing/generator/lib.php');
$elements = $this->get_creatable_entities();
+ foreach ($elements as $key => $configuration) {
+ if (array_key_exists('singular', $configuration)) {
+ $singularverb = $configuration['singular'];
+ unset($configuration['singular']);
+ unset($elements[$key]['singular']);
+ $elements[$singularverb] = $configuration;
+ }
+ }
+
if (!isset($elements[$generatortype])) {
throw new PendingException($this->name_for_errors($generatortype) .
' is not a known type of entity that can be generated.');
$generatortype = $entityinfo['datagenerator'];
- foreach ($data->getHash() as $elementdata) {
+ if ($singular) {
+ // There is only one record to generate, and the table has been pivotted.
+ // The rows each represent a single field.
+ $rows = [$data->getRowsHash()];
+ } else {
+ // There are multiple records to generate.
+ // The rows represent an item to create.
+ $rows = $data->getHash();
+ }
+ foreach ($rows as $elementdata) {
// Check if all the required fields are there.
foreach ($entityinfo['required'] as $requiredfield) {
if (!isset($elementdata[$requiredfield])) {
--- /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/>.
+
+/**
+ * A web service to load the mapping of moodle pix names to fontawesome icon names.
+ *
+ * @package core
+ * @category external
+ * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\external\output\icon_system;
+
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use core\output\icon_system_fontawesome;
+use theme_config;
+
+/**
+ * Web service to load font awesome icon maps.
+ *
+ * @package core
+ * @category external
+ * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class load_fontawesome_map extends external_api {
+
+ /**
+ * Description of the parameters suitable for the `execute` function.
+ *
+ * @return external_function_parameters
+ */
+ public static function execute_parameters() {
+ return new external_function_parameters([
+ 'themename' => new external_value(PARAM_ALPHANUMEXT, 'The theme to fetch the map for'),
+ ]);
+ }
+
+ /**
+ * Return a mapping of icon names to icons.
+ *
+ * @param string $themename The theme to fetch icons for
+ * @return array the mapping
+ */
+ public static function execute(string $themename) {
+ [
+ 'themename' => $themename,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'themename' => $themename,
+ ]);
+
+ $theme = theme_config::load($themename);
+ $instance = icon_system_fontawesome::instance($theme->get_icon_system());
+
+ $result = [];
+ foreach ($instance->get_icon_name_map() as $from => $to) {
+ [$component, $pix] = explode(':', $from);
+ $result[] = [
+ 'component' => $component,
+ 'pix' => $pix,
+ 'to' => $to,
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Description of the return value for the `execute` function.
+ *
+ * @return external_description
+ */
+ public static function execute_returns() {
+ return new external_multiple_structure(new external_single_structure([
+ 'component' => new external_value(PARAM_COMPONENT, 'The component for the icon.'),
+ 'pix' => new external_value(PARAM_RAW, 'Value to map the icon from.'),
+ 'to' => new external_value(PARAM_RAW, 'Value to map the icon to.'),
+ ]));
+ }
+}
use moodle_exception;
use context_system;
use theme_config;
+use core\external\output\icon_system\load_fontawesome_map;
/**
* This class contains a list of webservice functions related to output.
* @return array the mapping
*/
public static function load_fontawesome_icon_map() {
- $instance = icon_system::instance(icon_system::FONTAWESOME);
+ global $PAGE;
- $map = $instance->get_icon_name_map();
-
- $result = [];
-
- foreach ($map as $from => $to) {
- list($component, $pix) = explode(':', $from);
- $one = [];
- $one['component'] = $component;
- $one['pix'] = $pix;
- $one['to'] = $to;
- $result[] = $one;
- }
- return $result;
+ return load_fontawesome_map::execute($PAGE->theme->name);
}
/**
* @return external_description
*/
public static function load_fontawesome_icon_map_returns() {
- return new external_multiple_structure(new external_single_structure(
- array(
- 'component' => new external_value(PARAM_COMPONENT, 'The component for the icon.'),
- 'pix' => new external_value(PARAM_RAW, 'Value to map the icon from.'),
- 'to' => new external_value(PARAM_RAW, 'Value to map the icon to.')
- )
- ));
+ return load_fontawesome_map::execute_returns();
}
}
-
global $PAGE;
if (empty(self::$instance)) {
- $icontype = $PAGE->theme->get_icon_system();
- self::$instance = new $icontype();
+ $iconsystem = $PAGE->theme->get_icon_system();
+ self::$instance = new $iconsystem();
}
- // If $type is specified we need to make sure that the theme icon system supports this type,
- // if not, we will return a generic new instance of the $type.
- if ($type === null || is_a(self::$instance, $type)) {
+ if ($type === null) {
+ // No type specified. Return the icon system for the current theme.
+ return self::$instance;
+ }
+
+ if (!static::is_valid_system($type)) {
+ throw new \coding_exception("Invalid icon system requested '{$type}'");
+ }
+
+ if (is_a(self::$instance, $type) && is_a($type, get_class(self::$instance), true)) {
+ // The requested type is an exact match for the current icon system.
return self::$instance;
} else {
+ // Return the requested icon system.
return new $type();
}
}
* @return boolean
*/
public final static function is_valid_system($system) {
- return class_exists($system) && is_subclass_of($system, self::class);
+ return class_exists($system) && is_a($system, static::class, true);
}
/**
self::$instance = null;
}
}
-
* @copyright 2016 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class icon_system_standard {
+class icon_system_standard extends icon_system {
public function render_pix_icon(renderer_base $output, pix_icon $icon) {
$data = $icon->export_for_template($output);
'loginrequired' => false,
'ajax' => true,
),
+ 'core_output_load_fontawesome_icon_system_map' => array(
+ 'classname' => 'core\external\output\icon_system\load_fontawesome_map',
+ 'methodname' => 'execute',
+ 'description' => 'Load the mapping of moodle pix names to fontawesome icon names',
+ 'type' => 'read',
+ 'loginrequired' => false,
+ 'ajax' => true,
+ ),
// Question related functions.
'core_question_update_flag' => array(
'classname' => 'core_question_external',
upgrade_main_savepoint(true, 2020061500.02);
}
+ if ($oldversion < 2020061501.01) {
+ // Clean up completion criteria records referring to NULL course prerequisites.
+ $select = 'criteriatype = :type AND courseinstance IS NULL';
+ $params = ['type' => 8]; // COMPLETION_CRITERIA_TYPE_COURSE.
+
+ $DB->delete_records_select('course_completion_criteria', $select, $params);
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2020061501.01);
+ }
+
return true;
}
define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
/** Get remote addr constant */
define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
+/**
+ * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
+ */
+define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
// Blog access level constant declaration.
define ('BLOG_USER_LEVEL', 1);
if (empty($CFG->getremoteaddrconf)) {
// This will happen, for example, before just after the upgrade, as the
// user is redirected to the admin screen.
- $variablestoskip = 0;
+ $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
} else {
$variablestoskip = $CFG->getremoteaddrconf;
}
];
/**
- * Creates the specified element.
+ * Creates the specified elements.
*
* See the class comment for an overview.
*
$this->get_instance_for_component($component)->generate_items($entity, $data);
}
+ /**
+ * Creates the specified element.
+ *
+ * See the class comment for an overview.
+ *
+ * @Given the following :entitytype exists:
+ *
+ * @param string $entitytype The name of the type entity to add
+ * @param TableNode $data
+ */
+ public function the_following_entity_exists($entitytype, TableNode $data) {
+ if (isset($this->movedentitytypes[$entitytype])) {
+ $entitytype = $this->movedentitytypes[$entitytype];
+ }
+ list($component, $entity) = $this->parse_entity_type($entitytype);
+ $this->get_instance_for_component($component)->generate_items($entity, $data, true);
+ }
+
/**
* Parse a full entity type like 'users' or 'mod_forum > subscription'.
*
--- /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/>.
+
+/**
+ * External functions test for record_feedback_action.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\external\output\icon_system;
+
+use externallib_advanced_testcase;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+/**
+ * Class record_userfeedback_action_testcase
+ *
+ * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass core\external\output\icon_system\load_fontawesome_map
+ */
+class load_fontawesome_map_testcase extends externallib_advanced_testcase {
+
+ /**
+ * Perform setup before these tests are run.
+ */
+ public static function setUpBeforeClass(): void {
+ global $CFG;
+
+ // In normal operation the external_api classes will have been loaded by the caller.
+ // The load_fontawesome_map class should not need to supplement our lack of autoloading of these classes.
+ require_once($CFG->libdir . '/externallib.php');
+ }
+
+ /**
+ * Ensure that a valid theme which uses fontawesome returns a map.
+ *
+ * @covers ::execute_parameters
+ * @covers ::execute
+ * @covers ::execute_returns
+ * @dataProvider valid_fontawesome_theme_provider
+ * @param string $themename
+ */
+ public function test_execute(string $themename): void {
+ $result = load_fontawesome_map::execute($themename);
+ $this->assertIsArray($result);
+
+ foreach ($result as $value) {
+ $this->assertArrayHasKey('component', $value);
+ $this->assertArrayHasKey('pix', $value);
+ $this->assertArrayHasKey('to', $value);
+ }
+ }
+
+ /**
+ * Ensure that an invalid theme cannot be loaded.
+ */
+ public function test_execute_invalid_themename(): void {
+ $result = load_fontawesome_map::execute('invalidtheme');
+ $this->assertDebuggingCalled(
+ 'This page should be using theme invalidtheme which cannot be initialised. Falling back to the site theme boost'
+ );
+ $this->assertIsArray($result);
+ }
+
+ /**
+ * Data provider for valid themes to use with the execute function.
+ *
+ * @return array
+ */
+ public function valid_fontawesome_theme_provider(): array {
+ return [
+ 'Boost theme' => ['boost'],
+ 'Classic theme (extends boost)' => ['classic'],
+ ];
+ }
+}
--- /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/>.
+
+/**
+ * Unit tests for lib/outputcomponents.php.
+ *
+ * @package core
+ * @category test
+ * @copyright 2011 David Mudrak <david@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use advanced_testcase;
+use coding_exception;
+
+/**
+ * Unit tests for the `icon_system` class.
+ *
+ * @coversDefaultClass core\output\icon_system
+ */
+class icon_system_test extends advanced_testcase {
+ /**
+ * Check whether the supplied classes are valid icon subsystems of the supplied one.
+ *
+ * @covers ::is_valid_system
+ * @dataProvider is_valid_subsystem_provider
+ * @param string $parent The class to call ::is_valid_system() on
+ * @param string $system The class to request
+ * @param bool $expected Whether the supplied relationship is valid
+ */
+ public function test_is_valid_subsystem(string $parent, string $system, bool $expected): void {
+ $this->assertEquals($expected, $parent::is_valid_system($system));
+ }
+
+ /**
+ * Ensure that the ::instance() function throws an appropriate Exception when an inappropriate relationship is
+ * specified.
+ *
+ * @covers ::instance
+ * @dataProvider invalid_instance_provider
+ * @param string $parent The class to call ::instance() on
+ * @param string $system The class to request
+ */
+ public function test_invalid_instance(string $parent, string $system): void {
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("Invalid icon system requested '{$system}'");
+
+ $parent::instance($system);
+ }
+
+ /**
+ * Ensure that the ::instance() function returns an instance of the supplied system for a valid icon system
+ * relationship.
+ *
+ * @covers ::instance
+ * @dataProvider valid_instance_provider
+ * @param string $parent The class to call ::instance() on
+ * @param string $system The class to request
+ */
+ public function test_valid_instance(string $parent, string $system): void {
+ $instance = $parent::instance($system);
+ $this->assertInstanceOf($parent, $instance);
+ $this->assertInstanceOf($system, $instance);
+ }
+
+ /**
+ * Ensure that subsequent calls without arguments to ::instance() return the exact same instance.
+ *
+ * @covers ::instance
+ */
+ public function test_instance_singleton(): void {
+ $singleton = icon_system::instance();
+
+ // Calling instance() again returns the same singleton.
+ $this->assertSame($singleton, icon_system::instance());
+ }
+
+ /**
+ * Ensure thaat subsequent calls with an argument to ::instance() return the exact same instance.
+ *
+ * @covers ::instance
+ */
+ public function test_instance_singleton_named_default(): void {
+ global $PAGE;
+ $singleton = icon_system::instance();
+
+ $defaultsystem = $PAGE->theme->get_icon_system();
+ $this->assertSame($singleton, icon_system::instance($defaultsystem));
+ }
+
+ /**
+ * Ensure that ::instance() returns an instance of the correct icon system when requested on the core icon_system
+ * class.
+ *
+ * @covers ::instance
+ * @dataProvider valid_instance_provider
+ * @param string $parent The class to call ::instance() on
+ * @param string $child The class to request
+ */
+ public function test_instance_singleton_named(string $parent, string $child): void {
+ $iconsystem = icon_system::instance($child);
+ $this->assertInstanceOf($child, $iconsystem);
+ }
+
+ /**
+ * Ensure that ::instance() returns an instance of the correct icon system when called on a named parent class.
+ *
+ * @covers ::instance
+ * @dataProvider valid_instance_provider
+ * @param string $parent The class to call ::instance() on
+ * @param string $child The class to request
+ */
+ public function test_instance_singleton_named_child(string $parent, string $child): void {
+ $iconsystem = $parent::instance($child);
+ $this->assertInstanceOf($parent, $iconsystem);
+ $this->assertInstanceOf($child, $iconsystem);
+ }
+
+ /**
+ * Ensure that the ::reset_caches() function resets the stored instance such that ::instance() returns a new
+ * instance in subsequent calls.
+ *
+ * @covers ::instance
+ * @covers ::reset_caches
+ */
+ public function test_instance_singleton_reset(): void {
+ $singleton = icon_system::instance();
+
+ // Reset the cache.
+ icon_system::reset_caches();
+
+ // Calling instance() again returns a new singleton.
+ $newsingleton = icon_system::instance();
+ $this->assertNotSame($singleton, $newsingleton);
+
+ // Calling it again gets the new singleton.
+ $this->assertSame($newsingleton, icon_system::instance());
+ }
+
+ /**
+ * Returns data for data providers containing:
+ * - parent icon system
+ * - child icon system
+ * - whether it is a valid child
+ *
+ * @return array
+ */
+ public function icon_system_provider(): array {
+ return [
+ 'icon_system => icon_system_standard' => [
+ icon_system::class,
+ icon_system_standard::class,
+ true,
+ ],
+ 'icon_system => icon_system_fontawesome' => [
+ icon_system::class,
+ icon_system_fontawesome::class,
+ true,
+ ],
+ 'icon_system => \theme_classic\output\icon_system_fontawesome' => [
+ icon_system::class,
+ \theme_classic\output\icon_system_fontawesome::class,
+ true,
+ ],
+ 'icon_system => notification' => [
+ icon_system::class,
+ notification::class,
+ false,
+ ],
+
+ 'icon_system_standard => icon_system_standard' => [
+ icon_system_standard::class,
+ icon_system_standard::class,
+ true,
+ ],
+ 'icon_system_standard => icon_system_fontawesome' => [
+ icon_system_standard::class,
+ icon_system_fontawesome::class,
+ false,
+ ],
+ 'icon_system_standard => \theme_classic\output\icon_system_fontawesome' => [
+ icon_system_standard::class,
+ \theme_classic\output\icon_system_fontawesome::class,
+ false,
+ ],
+ 'icon_system_fontawesome => icon_system_standard' => [
+ icon_system_fontawesome::class,
+ icon_system_standard::class,
+ false,
+ ],
+ ];
+ }
+
+ /**
+ * Data provider for tests of `is_valid`.
+ *
+ * @return array
+ */
+ public function is_valid_subsystem_provider(): array {
+ return $this->icon_system_provider();
+ }
+
+ /**
+ * Data provider for tests of `instance` containing only invalid tests.
+ *
+ * @return array
+ */
+ public function invalid_instance_provider(): array {
+ return array_filter(
+ $this->icon_system_provider(),
+ function($data) {
+ return !$data[2];
+ },
+ ARRAY_FILTER_USE_BOTH
+ );
+ }
+
+ /**
+ * Data provider for tests of `instance` containing only valid tests.
+ *
+ * @return array
+ */
+ public function valid_instance_provider(): array {
+ return array_filter(
+ $this->icon_system_provider(),
+ function($data) {
+ return $data[2];
+ },
+ ARRAY_FILTER_USE_BOTH
+ );
+ }
+
+}
This files describes API changes in core libraries and APIs,
information provided here is intended especially for developers.
+=== 3.9.2 ===
+* A new web service `core_output_load_fontawesome_icon_system_map` has been created.
+ This replaces the existing `core_output_load_fontawesome_icon_map` web service, which will be deprecated in Moodle 3.10.
+
=== 3.9.1 ===
* The `$CFG->behat_retart_browser_after` configuration setting has been removed.
The browser session is now restarted between all tests.
'gradingstatus' => $assign->get_grading_status($submissionrecord->userid)
);
- if ($assign->can_view_submission($submissionrecord->userid)) {
+ if (($assign->get_instance()->teamsubmission
+ && $assign->can_view_group_submission($submissionrecord->groupid))
+ || (!$assign->get_instance()->teamsubmission
+ && $assign->can_view_submission($submissionrecord->userid))
+ ) {
$submissions[] = $submission;
}
}
// Icons.
$iconstr = '';
- if ($active) {
- // Edit.
- $editurlstr = $overrideediturl->out(true, array('id' => $override->id));
- $iconstr = '<a title="' . get_string('edit') . '" href="'. $editurlstr . '">' .
- $OUTPUT->pix_icon('t/edit', get_string('edit')) . '</a> ';
- // Duplicate.
- $copyurlstr = $overrideediturl->out(true,
- array('id' => $override->id, 'action' => 'duplicate'));
- $iconstr .= '<a title="' . get_string('copy') . '" href="' . $copyurlstr . '">' .
- $OUTPUT->pix_icon('t/copy', get_string('copy')) . '</a> ';
- }
+ // Edit.
+ $editurlstr = $overrideediturl->out(true, array('id' => $override->id));
+ $iconstr = '<a title="' . get_string('edit') . '" href="'. $editurlstr . '">' .
+ $OUTPUT->pix_icon('t/edit', get_string('edit')) . '</a> ';
+ // Duplicate.
+ $copyurlstr = $overrideediturl->out(true,
+ array('id' => $override->id, 'action' => 'duplicate'));
+ $iconstr .= '<a title="' . get_string('copy') . '" href="' . $copyurlstr . '">' .
+ $OUTPUT->pix_icon('t/copy', get_string('copy')) . '</a> ';
// Delete.
$deleteurlstr = $overridedeleteurl->out(true,
array('id' => $override->id, 'sesskey' => sesskey()));
And I navigate to "User overrides" in current page administration
Then I should see "Student1" in the ".generaltable" "css_element"
And I should not see "Student2" in the ".generaltable" "css_element"
+
+ @javascript
+ Scenario: Create a user override when the assignment is not available to the student
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage with editing mode on
+ And I follow "Test assignment name"
+ And I navigate to "Edit settings" in current page administration
+ And I expand all fieldsets
+ And I set the field "Availability" to "Hide from students"
+ And I click on "Save and display" "button"
+ When I navigate to "User overrides" in current page administration
+ And I press "Add user override"
+ And I set the following fields to these values:
+ | Override user | Student1 |
+ | id_allowsubmissionsfromdate_enabled | 1 |
+ | allowsubmissionsfromdate[day] | 1 |
+ | allowsubmissionsfromdate[month] | January |
+ | allowsubmissionsfromdate[year] | 2015 |
+ | allowsubmissionsfromdate[hour] | 08 |
+ | allowsubmissionsfromdate[minute] | 00 |
+ And I press "Save"
+ Then I should see "This override is inactive"
+ And "Edit" "icon" should exist in the "Sam1 Student1" "table_row"
+ And "copy" "icon" should exist in the "Sam1 Student1" "table_row"
+ And "Delete" "icon" should exist in the "Sam1 Student1" "table_row"
$this->assertEquals(1, count($result['assignments']));
}
+ public function test_get_submissions_group_submission() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+
+ $result = $this->create_assign_with_student_and_teacher(array(
+ 'assignsubmission_onlinetext_enabled' => 1,
+ 'teamsubmission' => 1
+ ));
+ $assignmodule = $result['assign'];
+ $student = $result['student'];
+ $teacher = $result['teacher'];
+ $course = $result['course'];
+ $context = context_course::instance($course->id);
+ $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+ $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+ $cm = get_coursemodule_from_instance('assign', $assignmodule->id);
+ $context = context_module::instance($cm->id);
+ $assign = new mod_assign_testable_assign($context, $cm, $course);
+
+ groups_add_member($group, $student);
+
+ $this->setUser($student);
+ $submission = $assign->get_group_submission($student->id, $group->id, true);
+ $sid = $submission->id;
+
+ $this->setUser($teacher);
+
+ $assignmentids[] = $assignmodule->id;
+ $result = mod_assign_external::get_submissions($assignmentids);
+ $result = external_api::clean_returnvalue(mod_assign_external::get_submissions_returns(), $result);
+
+ $this->assertEquals(1, count($result['assignments']));
+ $assignment = $result['assignments'][0];
+ $this->assertEquals($assignmodule->id, $assignment['assignmentid']);
+ $this->assertEquals(1, count($assignment['submissions']));
+ $submission = $assignment['submissions'][0];
+ $this->assertEquals($sid, $submission['id']);
+ $this->assertEquals($group->id, $submission['groupid']);
+ $this->assertEquals(0, $submission['userid']);
+ }
+
/**
* Test get_user_flags
*/
// Icons.
$iconstr = '';
- if ($active) {
- // Edit.
- $editurlstr = $overrideediturl->out(true, array('id' => $override->id));
- $iconstr = '<a title="' . get_string('edit') . '" href="'. $editurlstr . '">' .
- $OUTPUT->pix_icon('t/edit', get_string('edit')) . '</a> ';
- // Duplicate.
- $copyurlstr = $overrideediturl->out(true,
- array('id' => $override->id, 'action' => 'duplicate'));
- $iconstr .= '<a title="' . get_string('copy') . '" href="' . $copyurlstr . '">' .
- $OUTPUT->pix_icon('t/copy', get_string('copy')) . '</a> ';
- }
+ // Edit.
+ $editurlstr = $overrideediturl->out(true, array('id' => $override->id));
+ $iconstr = '<a title="' . get_string('edit') . '" href="'. $editurlstr . '">' .
+ $OUTPUT->pix_icon('t/edit', get_string('edit')) . '</a> ';
+ // Duplicate.
+ $copyurlstr = $overrideediturl->out(true,
+ array('id' => $override->id, 'action' => 'duplicate'));
+ $iconstr .= '<a title="' . get_string('copy') . '" href="' . $copyurlstr . '">' .
+ $OUTPUT->pix_icon('t/copy', get_string('copy')) . '</a> ';
// Delete.
$deleteurlstr = $overridedeleteurl->out(true,
array('id' => $override->id, 'sesskey' => sesskey()));
And I log in as "student2"
And I am on "Course 1" course homepage
And I follow "Test lesson"
+ And I wait until the page is ready
Then I should see "This lesson closed on Saturday, 1 January 2000, 8:00"
And I should not see "Cat is an amphibian"
And I log out
And I log in as "student2"
And I am on "Course 1" course homepage
And I follow "Test lesson"
- Then I should see "This lesson will be open on Tuesday, 1 January 2030, 8:00"
+ And I wait until the page is ready
+ Then I should see "This lesson will be open on Tuesday, 1 January 2030, 8:00"
And I should not see "Cat is an amphibian"
And I log out
And I log in as "student1"
And I navigate to "User overrides" in current page administration
Then I should see "Student1" in the ".generaltable" "css_element"
And I should not see "Student2" in the ".generaltable" "css_element"
+
+ @javascript
+ Scenario: Create a user override when the lesson is not available to the student
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage with editing mode on
+ And I follow "Test lesson name"
+ And I navigate to "Edit settings" in current page administration
+ And I expand all fieldsets
+ And I set the field "Availability" to "Hide from students"
+ And I click on "Save and display" "button"
+ When I navigate to "User overrides" in current page administration
+ And I press "Add user override"
+ And I set the following fields to these values:
+ | Override user | Student1 |
+ | Maximum number of attempts | 2 |
+ And I press "Save"
+ Then I should see "This override is inactive"
+ And "Edit" "icon" should exist in the "Sam1 Student1" "table_row"
+ And "copy" "icon" should exist in the "Sam1 Student1" "table_row"
+ And "Delete" "icon" should exist in the "Sam1 Student1" "table_row"
protected function get_creatable_entities(): array {
return [
'seb templates' => [
+ 'singular' => 'seb template',
'datagenerator' => 'template',
'required' => ['name'],
],
pageno = 0;
}
- var questionidmatch = this.get('href').match(/#q(\d+)/);
+ var questionidmatch = this.get('href').match(/#question-(\d+)-(\d+)/);
if (questionidmatch) {
- form.set('action', form.get('action') + '#q' + questionidmatch[1]);
+ form.set('action', form.get('action') + questionidmatch[0]);
}
nav_to_page(pageno);
| Override user | Student1 |
| Attempts allowed | 1 |
And I press "Save"
- Then "Edit" "icon" should exist in the "Student One" "table_row"
+ Then I should see "This override is inactive"
+ And "Edit" "icon" should exist in the "Student One" "table_row"
+ And "copy" "icon" should exist in the "Student One" "table_row"
+ And "Delete" "icon" should exist in the "Student One" "table_row"
Scenario: A teacher without accessallgroups permission should only be able to add user override for users that he/she shares groups with,
when the activity's group mode is to "separate groups"
protected function get_creatable_entities(): array {
return [
'group overrides' => [
+ 'singular' => 'group override',
'datagenerator' => 'override',
'required' => ['quiz', 'group'],
'switchids' => ['quiz' => 'quiz', 'group' => 'groupid'],
],
'user overrides' => [
+ 'singular' => 'user override',
'datagenerator' => 'override',
'required' => ['quiz', 'user'],
'switchids' => ['quiz' => 'quiz', 'user' => 'userid'],
* @param slot the number of the question_attempt within the usage.
*/
M.core_question_engine.init_submit_button = function(Y, button, slot) {
+ var totalQuestionsInPage = document.querySelectorAll('div.que').length;
var buttonel = document.getElementById(button);
+ var outeruniqueid = buttonel.closest('.que').id;
Y.on('click', function(e) {
M.core_scroll_manager.save_scroll_pos(Y, button);
- buttonel.form.action = buttonel.form.action + '#q' + slot;
+ if (totalQuestionsInPage > 1) {
+ // Only change the form action if the page have more than one question.
+ buttonel.form.action = buttonel.form.action + '#' + outeruniqueid;
+ }
}, buttonel);
}
// are generated by behat_core_generator.
return [
'Tags' => [
+ 'singular' => 'Tag',
'datagenerator' => 'question_tag',
'required' => ['question', 'tag'],
'switchids' => ['question' => 'questionid'],
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | S1 | Student1 | student1@moodle.com |
- And the following "courses" exist:
- | fullname | shortname | category |
- | Course 1 | C1 | 0 |
+ And the following "course" exists:
+ | fullname | Course 1 |
+ | shortname | C1 |
+ | category | 0 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
- And the following "question categories" exist:
- | contextlevel | reference | name |
- | Course | C1 | Test questions |
- And the following "questions" exist:
- | questioncategory | qtype | name | template | questiontext |
- | Test questions | multichoice | Multi-choice-001 | one_of_four | Question One |
- And the following "activities" exist:
- | activity | name | intro | course | idnumber | preferredbehaviour | canredoquestions |
- | quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | 1 |
+ And the following "question category" exists:
+ | contextlevel | Course |
+ | reference | C1 |
+ | name | Test questions |
+ And the following "question" exists:
+ | questioncategory | Test questions |
+ | qtype | multichoice |
+ | name | Multi-choice-001 |
+ | template | one_of_four |
+ | questiontext | Question One |
+ And the following "activity" exists:
+ | activity | quiz |
+ | name | Quiz 1 |
+ | intro | Quiz 1 description |
+ | course | C1 |
+ | idnumber | quiz1 |
+ | preferredbehaviour | immediatefeedback |
+ | canredoquestions | 1 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Multi-choice-001 | 1 |
.listitem {
&[data-selected='1'] {
- border-left: calc(#{$list-group-border-width} + 5px) solid map-get($theme-colors, 'info');
+ border-left: calc(#{$list-group-border-width} + 5px) solid map-get($theme-colors, 'primary');
padding-left: calc(#{$list-group-item-padding-x} - 5px);
}
}
#course-category-listings ul.ml ul.ml {
margin: 0; }
#course-category-listings .listitem[data-selected='1'] {
- border-left: calc(1px + 5px) solid #5bc0de;
+ border-left: calc(1px + 5px) solid #1177d1;
padding-left: calc(1.25rem - 5px); }
#course-category-listings .item-actions {
margin-right: 1em;
#course-category-listings ul.ml ul.ml {
margin: 0; }
#course-category-listings .listitem[data-selected='1'] {
- border-left: calc(1px + 5px) solid #5bc0de;
+ border-left: calc(1px + 5px) solid #1177d1;
padding-left: calc(1.25rem - 5px); }
#course-category-listings .item-actions {
margin-right: 1em;
$forcedgroupjoin = groups_get_members_join($allowedgroupids, $forceduid, $this->context, $forcedjointype);
$forcedjoins[] = $forcedgroupjoin->joins;
- $forcedwhere .= "AND ({$forcedgroupjoin->wheres})";
+ $forcedwhere .= " AND ({$forcedgroupjoin->wheres})";
$params = array_merge($params, $forcedgroupjoin->params);
defined('MOODLE_INTERNAL') || die();
-$version = 2020061501.00; // 20200615 = branching date YYYYMMDD - do not modify!
+$version = 2020061501.02; // 20200615 = branching date YYYYMMDD - do not modify!
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '3.9.1 (Build: 20200713)'; // Human-friendly version name
+$release = '3.9.1+ (Build: 20200717)'; // Human-friendly version name
$branch = '39'; // This version's branch.
$maturity = MATURITY_STABLE; // This version's maturity level.