$string['browsecontentmoodlenet'] = "Or browse for content on MoodleNet";
$string['clearsearch'] = "Clear search";
$string['connectandbrowse'] = "Connect to and browse:";
-$string['defaultmoodlenet'] = "Default MoodleNet URL";
+$string['defaultmoodlenet'] = 'MoodleNet URL';
$string['defaultmoodlenet_desc'] = "The URL to either Moodle HQ's MoodleNet instance, or your preferred instance.";
$string['defaultmoodlenetname'] = "MoodleNet instance name";
-$string['defaultmoodlenetname_desc'] = 'The name of either Moodle HQ\'s MoodleNet instance or your preferred MoodleNet instance to browse on.';
+$string['defaultmoodlenetnamevalue'] = 'MoodleNet Home';
+$string['defaultmoodlenetname_desc'] = 'The name of the MoodleNet instance available via the activity chooser.';
$string['enablemoodlenet'] = 'Enable MoodleNet integration';
-$string['enablemoodlenet_desc'] = 'Enabling the integration allows users with the \'xx\' capability to browse MoodleNet from the
-activity chooser and import MoodleNet resources into their course. It also allows users to push backups from MoodleNet into Moodle.
-';
+$string['enablemoodlenet_desc'] = 'If enabled, a user with the capability to create and manage activities can browse MoodleNet via the activity chooser and import MoodleNet resources into their course. In addition, a user with the capability to restore backups can select a backup file on MoodleNet and restore it into Moodle.';
$string['errorduringdownload'] = 'An error occurred while downloading the file: {$a}';
-$string['forminfo'] = "It will be automatically saved on your moodle profile.";
+$string['forminfo'] = 'Your MoodleNet profile will be automatically saved in your profile on this site.';
$string['footermessage'] = "Or browse for content on";
$string['instancedescription'] = "MoodleNet is an open social media platform for educators, with a focus on the collaborative curation of collections of open resources. ";
$string['instanceplaceholder'] = '@yourprofile@moodle.net';
$string['invalidmoodlenetprofile'] = '$userprofile is not correctly formatted';
$string['importconfirm'] = 'You are about to import the content "{$a->resourcename} ({$a->resourcetype})" into the course "{$a->coursename}". Are you sure you want to continue?';
$string['importconfirmnocourse'] = 'You are about to import the content "{$a->resourcename} ({$a->resourcetype})" into your site. Are you sure you want to continue?';
-$string['importformatselectguidingtext'] = 'In which format would you like this content "{$a->name} ({$a->type})" to be added to your course?';
+$string['importformatselectguidingtext'] = 'In which format would you like the content "{$a->name} ({$a->type})" to be added to your course?';
$string['importformatselectheader'] = 'Choose the content display format';
$string['missinginvalidpostdata'] = 'The resource information from MoodleNet is either missing, or is in an incorrect format.
If this happens repeatedly, please contact the site administrator.';
$string['mnetprofile'] = 'MoodleNet profile';
-$string['mnetprofiledesc'] = '<p>Enter in your MoodleNet profile details here to be redirected to your profile while visiting MoodleNet.</p>';
+$string['mnetprofiledesc'] = '<p>Enter your MoodleNet profile details here to be redirected to your profile while visiting MoodleNet.</p>';
$string['moodlenetsettings'] = 'MoodleNet settings';
-$string['moodlenetnotenabled'] = 'The MoodleNet integration must be enabled before resource imports can be processed.
-To enable this feature, see the \'enablemoodlenet\' setting.';
+$string['moodlenetnotenabled'] = 'The MoodleNet integration must be enabled in Site administration / MoodleNet before resource imports can be processed.';
$string['notification'] = 'You are about to import the content "{$a->name} ({$a->type})" into your site. Select the course in which it should be added, or <a href="{$a->cancellink}">cancel</a>.';
$string['searchcourses'] = "Search courses";
$string['selectpagetitle'] = 'Select page';
$temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenetname',
get_string('defaultmoodlenetname', 'tool_moodlenet'), new lang_string('defaultmoodlenetname_desc', 'tool_moodlenet'),
- 'Moodle HQ MoodleNet');
+ new lang_string('defaultmoodlenetnamevalue', 'tool_moodlenet'));
$settings->add($temp);
$temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenet', get_string('defaultmoodlenet', 'tool_moodlenet'),
}
if (empty($lastcron)) {
- $summary = get_string('cronwarningnever', 'admin', [
- 'expected' => $formatexpected,
- ]);
+ if (empty($CFG->cronclionly)) {
+ $url = new \moodle_url('/admin/cron.php');
+ $summary = get_string('cronwarningneverweb', 'admin', [
+ 'url' => $url->out(),
+ 'expected' => $formatexpected,
+ ]);
+ } else {
+ $summary = get_string('cronwarningnever', 'admin', [
+ 'expected' => $formatexpected,
+ ]);
+ }
} else if (empty($CFG->cronclionly)) {
$url = new \moodle_url('/admin/cron.php');
$summary = get_string('cronwarning', 'admin', [
*/
export const init = () => {
const contentBank = document.querySelector(selectors.regions.contentbank);
- Prefetch.prefetchStrings('contentbank', ['sortbyx', 'sortbyxreverse', 'contentname',
- 'lastmodified', 'size', 'type']);
+ Prefetch.prefetchStrings('contentbank', ['contentname', 'lastmodified', 'size', 'type']);
+ Prefetch.prefetchStrings('moodle', ['sortbyx', 'sortbyxreverse']);
registerListenerEvents(contentBank);
};
/**
* Returns whether or not the user has permission to use the editor.
+ * This function will be called with the content to be edited as parameter,
+ * or null when is checking permission to create a new content using the editor.
*
+ * @param content $content The content to be edited or null when creating a new content.
* @return bool True if the user can edit content. False otherwise.
*/
- final public function can_edit(): bool {
+ final public function can_edit(?content $content = null): bool {
if (!$this->is_feature_supported(self::CAN_EDIT)) {
return false;
}
return false;
}
+ if (!is_null($content) && !$this->can_manage($content)) {
+ return false;
+ }
+
$classname = 'contenttype/'.$this->get_plugin_name();
$editioncap = $classname.':useeditor';
$hascapabilities = has_all_capabilities(['moodle/contentbank:useeditor', $editioncap], $this->context);
- return $hascapabilities && $this->is_edit_allowed();
+ return $hascapabilities && $this->is_edit_allowed($content);
}
/**
* Returns plugin allows edition.
*
+ * @param content $content The content to be edited.
* @return bool True if plugin allows edition. False otherwise.
*/
- protected function is_edit_allowed(): bool {
+ protected function is_edit_allowed(?content $content): bool {
// Plugins can overwrite this function to add any check they need.
return true;
}
$data->contenthtml = $contenthtml;
// Check if the user can edit this content type.
- if ($this->contenttype->can_edit()) {
+ if ($this->contenttype->can_edit($this->content)) {
$data->usercanedit = true;
$urlparams = [
'contextid' => $this->content->get_contextid(),
} else {
$contenttypename = "contenttype_$pluginname";
$heading = get_string('addinganew', 'moodle', get_string('description', $contenttypename));
+ $content = null;
}
// Check plugin is enabled.
print_error('unsupported', 'core_contentbank', $returnurl);
}
-// Checks the user can edit this content type.
-if (!$contenttype->can_edit()) {
- print_error('contenttypenoedit', 'core_contentbank', $returnurl, $contenttype->get_plugin_name());
+// Checks the user can edit this content and content type.
+if (!$contenttype->can_edit($content)) {
+ print_error('contenttypenoedit', 'core_contentbank', $returnurl);
}
$values = [
And I click on "Edit" "link"
And I switch to "h5p-editor-iframe" class iframe
Then the field "Title" matches value "New title"
+
+ Scenario: Teachers can edit their own content in the content bank
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And the following "contentbank content" exist:
+ | contextlevel | reference | contenttype | user | contentname | filepath |
+ | Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
+ | Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p |
+ When I log out
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I expand "Site pages" node
+ And I click on "Content bank" "link"
+ And I follow "ipsums.h5p"
+ Then "Edit" "link" should exist in the "region-main" "region"
+
+ Scenario: Teachers can't edit content created by other users in the content bank
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And the following "contentbank content" exist:
+ | contextlevel | reference | contenttype | user | contentname | filepath |
+ | Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
+ | Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p |
+ When I log out
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I expand "Site pages" node
+ And I click on "Content bank" "link"
+ And I follow "filltheblanks.h5p"
+ Then "Edit" "link" should not exist in the "region-main" "region"
{{>core_course/local/activitychooser/search}}
</div>
<div data-region="chooser-container" class="chooser-container">
- <div class="nav nav-tabs z-index-1" id="activities-{{uniqid}}" role="tablist">
+ <div class="nav nav-tabs flex-shrink-0 z-index-1" id="activities-{{uniqid}}" role="tablist">
<a class="nav-item nav-link {{#favouritesFirst}}active{{/favouritesFirst}} {{^favourites}}d-none{{/favourites}}"
id="starred-tab-{{uniqid}}"
data-toggle="tab"
// Get the content items using both the live and the caching repos.
$items = $cir->find_all_for_course($course, $user);
$cacheditems = $ccir->find_all_for_course($course, $user);
- $itemsfiltered = array_filter($items, function($item) {
- return $item->get_component_name() == 'mod_assign';
- });
- $cacheditemsfiltered = array_filter($cacheditems, function($item) {
- return $item->get_component_name() == 'mod_assign';
- });
+ $itemsfiltered = array_values(array_filter($items, function($item) {
+ return $item->get_component_name() == 'mod_book';
+ }));
+ $cacheditemsfiltered = array_values(array_filter($cacheditems, function($item) {
+ return $item->get_component_name() == 'mod_book';
+ }));
- // Verify the assign module is in both result sets.
- $module = $DB->get_record('modules', ['name' => 'assign']);
+ // Verify the book module is in both result sets.
+ $module = $DB->get_record('modules', ['name' => 'book']);
$this->assertEquals($module->name, $itemsfiltered[0]->get_name());
$this->assertEquals($module->name, $cacheditemsfiltered[0]->get_name());
$DB->set_field("modules", "visible", "0", ["id" => $module->id]);
$items = $cir->find_all_for_course($course, $user);
$cacheditems = $ccir->find_all_for_course($course, $user);
- $itemsfiltered = array_filter($items, function($item) {
- return $item->get_component_name() == 'mod_assign';
- });
- $cacheditemsfiltered = array_filter($cacheditems, function($item) {
- return $item->get_component_name() == 'mod_assign';
- });
+ $itemsfiltered = array_values(array_filter($items, function($item) {
+ return $item->get_component_name() == 'mod_book';
+ }));
+ $cacheditemsfiltered = array_values(array_filter($cacheditems, function($item) {
+ return $item->get_component_name() == 'mod_book';
+ }));
// The caching repo should return the same list, while the live repo will return the updated list.
$this->assertEquals($module->name, $cacheditemsfiltered[0]->get_name());
throw new coding_exception('Missing H5P library.');
}
- if ($content->h5plibrary != $this->library) {
- throw new coding_exception("Wrong H5P library.");
- }
-
$content->params = $content->h5pparams;
if (!empty($this->oldcontent)) {
if ($file) {
$fields['contenthash'] = $file->get_contenthash();
- // Delete old file if any.
- if (!empty($this->oldfile)) {
- $this->oldfile->delete();
- }
- // Create new file.
+ // Create or update H5P file.
if (empty($this->filearea['filename'])) {
$this->filearea['filename'] = $contentarray['slug'] . '.h5p';
}
- $newfile = $fs->create_file_from_storedfile($this->filearea, $file);
+ if (!empty($this->oldfile)) {
+ $this->oldfile->replace_file_with($file);
+ $newfile = $this->oldfile;
+ } else {
+ $newfile = $fs->create_file_from_storedfile($this->filearea, $file);
+ }
if (empty($this->oldcontent)) {
$pathnamehash = $newfile->get_pathnamehash();
} else {
'crossorigin' => null,
'libraryConfig' => $core->h5pF->getLibraryConfig(),
'pluginCacheBuster' => self::get_cache_buster(),
- 'libraryUrl' => autoloader::get_h5p_core_library_url('core/js')
+ 'libraryUrl' => autoloader::get_h5p_core_library_url('js')->out(),
);
return $settings;
$value = null;
$h5pversion = static::get_h5p_version();
$component = 'h5plib_v' . $h5pversion;
+ // Composed code languages, such as 'Spanish, Mexican' are different in H5P and Moodle:
+ // - In H5P, they use '-' to separate language from the country. For instance: es-mx.
+ // - However, in Moodle, they have '_' instead of '-'. For instance: es_mx.
+ $language = str_replace('-', '_', $language);
if (get_string_manager()->string_exists($identifier, $component)) {
$defaultmoodlelang = 'en';
// In Moodle, all the English strings always will exist because they have to be declared in order to let users
$string['editor:changefile'] = 'Change file';
$string['editor:changelanguage'] = 'Change language to :language?';
$string['editor:changelibrary'] = 'Change content type?';
-$string['editor:changelogdescription'] = 'Some licenses require that changes made to the original work, or derivatives are logged and displayed. You may log your changes here for licensing reasons or just to allow yourself and others to keep track of the changes made to this content.';
+$string['editor:changelogdescription'] = 'Some licences require that changes made to the original work, or derivatives are logged and displayed. You may log your changes here for licensing reasons or just to allow yourself and others to keep track of the changes made to this content.';
$string['editor:close'] = 'Close';
$string['editor:commonfields'] = 'Text overrides and translations';
$string['editor:commonfieldsdescription'] = 'Here you can edit settings or translate texts used in this content.';
$string['editor:contenttypeinstallerror'] = ':contentType could not be installed. Contact your administrator.';
$string['editor:contenttypeinstallsuccess'] = ':contentType successfully installed!';
$string['editor:contenttypeinstallingbuttonlabel'] = 'Installing';
-$string['editor:contenttypelicensepaneltitle'] = 'License';
+$string['editor:contenttypelicensepaneltitle'] = 'Licence';
$string['editor:contenttypenotinstalled'] = 'Content type not installed';
$string['editor:contenttypenotinstalleddesc'] = 'You do not have permission to install content types.';
$string['editor:contenttypeowner'] = 'By :owner';
$string['cronwarning'] = 'The <a href="{$a->url}">admin/cron.php script</a> has not been run for {$a->actual} and should run every {$a->expected}.';
$string['cronwarningcli'] = 'The <code>admin/cli/cron.php</code> script has not been run for {$a->actual} and should run every {$a->expected}.';
$string['cronwarningnever'] = 'The <code>admin/cli/cron.php</code> script has never been run and should run every {$a->expected}.';
+$string['cronwarningneverweb'] = 'The <a href="{$a->url}">admin/cron.php script</a> has never been run and should run every {$a->expected}.';
$string['ctyperequired'] = 'The ctype PHP extension is now required by Moodle, in order to improve site performance and to offer multilingual compatibility.';
$string['curlsecurityallowedport'] = 'cURL allowed ports list';
$string['curlsecurityallowedportsyntax'] = 'List of port numbers that cURL can connect to. Valid entries are integer numbers only. Put each entry on a new line. If left empty, then all ports are allowed. If set, in almost all cases, both 443 and 80 should be specified for cURL to connect to standard HTTPS and HTTP ports.';
$string['enablesearchareas'] = 'Enable search areas';
$string['enablestats'] = 'Enable statistics';
$string['enabletrusttext'] = 'Enable trusted content';
-$string['enableuserfeedback'] = 'Enable feedback about Moodle';
-$string['enableuserfeedback_desc'] = 'If enabled, a \'Give feedback\' link is displayed in a Dashboard alert and in the footer for users to give feedback about the Moodle LMS to Moodle HQ. The Dashboard alert also has a \'Remind me later\' option.';
+$string['enableuserfeedback'] = 'Enable feedback about this software';
+$string['enableuserfeedback_desc'] = 'If enabled, a \'Give feedback about this software\' link is displayed in a Dashboard alert and in the footer for users to give feedback about Moodle to Moodle HQ. The Dashboard alert also has a \'Remind me later\' option.';
$string['enablewebservices'] = 'Enable web services';
$string['enablewsdocumentation'] = 'Web services documentation';
$string['enrolinstancedefaults'] = 'Enrolment instance defaults';
$string['contentrenamed'] = 'The content has been renamed.';
$string['contentsmoved'] = 'Content bank contents moved to {$a}.';
$string['contenttypenoaccess'] = 'You cannot view this {$a} instance.';
-$string['contenttypenoedit'] = 'You cannot edit contents of the {$a} content type.';
+$string['contenttypenoedit'] = 'You can not edit this content';
$string['eventcontentcreated'] = 'Content created';
$string['eventcontentdeleted'] = 'Content deleted';
$string['eventcontentupdated'] = 'Content updated';
$string['privacy:metadata:files:filename'] = 'The name of the file in its file area';
$string['privacy:metadata:files:filepath'] = 'The path to the file in its file area';
$string['privacy:metadata:files:filesize'] = 'The size of the file';
-$string['privacy:metadata:files:license'] = 'The license of the file\'s content';
+$string['privacy:metadata:files:license'] = 'The licence of the file\'s content';
$string['privacy:metadata:files:mimetype'] = 'The MIME type of the file';
$string['privacy:metadata:files:source'] = 'The source of the file';
$string['privacy:metadata:files:timecreated'] = 'The time when the file was created';
$string['addedandupdatedss'] = 'Added {$a->%new} new H5P library and updated {$a->%old} old one.';
$string['addednewlibraries'] = 'Added {$a->%new} new H5P libraries.';
$string['addednewlibrary'] = 'Added {$a->%new} new H5P library.';
-$string['additionallicenseinfo'] = 'Any additional information about the license';
+$string['additionallicenseinfo'] = 'Any additional information about the licence';
$string['atto_h5p'] = 'Insert H5P button';
$string['atto_h5p_description'] = 'The Insert H5P button in the Atto editor enables users to insert H5P content by either entering a URL or embed code, or by uploading an H5P file.';
$string['author'] = 'Author';
$string['invalidstring'] = 'Provided string is not valid according to regexp in semantics. (value: "{$a->%value}", regexp: "{$a->%regexp}")';
$string['librarydirectoryerror'] = 'Library directory name must match machineName or machineName-majorVersion.minorVersion (from library.json). (Directory: {$a->%directoryName} , machineName: {$a->%machineName}, majorVersion: {$a->%majorVersion}, minorVersion: {$a->%minorVersion})';
$string['librariesmanagerdescription'] = '<p>H5P enables users to create interactive content by providing a range of content types.</p><p>To ensure that only trusted H5P content types are used on your site, you need to <i>either</i></p><ul><li>Upload H5P content types from h5p.org <i>or</i></li><li>Enable the scheduled task \'Download available H5P content types from h5p.org\'</li></ul><p>Note that users will only be able to use the H5P content types which are installed on your site.</p>';
-$string['license'] = 'License';
+$string['license'] = 'Licence';
$string['licenseCC010'] = 'CC0 1.0 Universal (CC0 1.0) Public Domain Dedication';
$string['licenseCC010U'] = 'CC0 1.0 Universal';
$string['licenseCC10'] = '1.0 Generic';
$string['licenseV2'] = 'Version 2';
$string['licenseV3'] = 'Version 3';
$string['licensee'] = 'Licensee';
-$string['licenseextras'] = 'License extras';
-$string['licenseversion'] = 'License version';
+$string['licenseextras'] = 'Licence extras';
+$string['licenseversion'] = 'Licence version';
$string['lockh5pdeploy'] = 'This H5P content cannot be accessed because it is being deployed. Please try again later.';
$string['missingcontentfolder'] = 'A valid content folder is missing';
$string['missingcoreversion'] = 'The system was unable to install the {$a->%component} component from the package, as it requires a newer version of the H5P plugin. This site is currently running version {$a->%current}, whereas the required version is {$a->%required} or higher. Please upgrade and then try again.';
$string['clialreadyinstalled'] = 'The configuration file config.php already exists. Please use admin/cli/install_database.php to upgrade Moodle for this site.';
$string['cliinstallfinished'] = 'Installation completed successfully.';
$string['cliinstallheader'] = 'Moodle {$a} command line installation program';
-$string['climustagreelicense'] = 'In non interactive mode you must agree to license by specifying --agree-license option';
+$string['climustagreelicense'] = 'In non-interactive mode you must agree to the licence by specifying --agree-license option';
$string['cliskipdatabase'] = 'Skipping database installation.';
$string['clitablesexist'] = 'Database tables already present; CLI installation cannot continue.';
$string['compatibilitysettings'] = 'Checking your PHP settings ...';
$string['byname'] = 'by {$a}';
$string['bypassed'] = 'Bypassed';
$string['cachecontrols'] = 'Cache controls';
-$string['calltofeedback'] = 'Moodle HQ would like your feedback on the Moodle LMS.';
-$string['calltofeedback_give'] = 'Give feedback';
+$string['calltofeedback'] = 'The creators of this software would like your feedback.';
+$string['calltofeedback_give'] = 'Give feedback about this software';
$string['calltofeedback_remind'] = 'Remind me later';
$string['cancel'] = 'Cancel';
$string['cancelled'] = 'Cancelled';
'analytics', 'availabilityconditions', 'behat', 'capability', 'cohortroles', 'customlang',
'dataprivacy', 'dbtransfer', 'filetypes', 'generator', 'health', 'httpsreplace', 'innodb',
'installaddon', 'langimport', 'licensemanager', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound',
- 'mobile', 'multilangupgrade', 'monitor', 'oauth2', 'phpunit', 'policy', 'profiling', 'recyclebin',
+ 'mobile', 'moodlenet', 'multilangupgrade', 'monitor', 'oauth2', 'phpunit', 'policy', 'profiling', 'recyclebin',
'replace', 'spamcleaner', 'task', 'templatelibrary', 'uploadcourse', 'uploaduser', 'unsuproles',
'usertours', 'xmldb'
),
public static function should_display_reminder(): bool {
global $CFG;
- if ($CFG->enableuserfeedback && isloggedin() && !isguestuser()) {
+ if (static::can_give_feedback()) {
$give = get_user_preferences('core_userfeedback_give');
$remind = get_user_preferences('core_userfeedback_remind');
return $url;
}
+ /**
+ * Whether the current can give feedback.
+ *
+ * @return bool
+ */
+ public static function can_give_feedback(): bool {
+ global $CFG;
+
+ return $CFG->enableuserfeedback && isloggedin() && !isguestuser();
+ }
+
/**
* Returns the last major upgrade time
*
// Add the image preview.
'<div class="mdl-align">' +
'<div class="{{CSS.IMAGEPREVIEWBOX}}">' +
- '<img src="#" class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
+ '<img class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
'</div>' +
// Add the submit button and close the form.
$string['advanced_dlg:about_author'] = 'Author';
$string['advanced_dlg:about_general'] = 'About';
$string['advanced_dlg:about_help'] = 'Help';
-$string['advanced_dlg:about_license'] = 'License';
+$string['advanced_dlg:about_license'] = 'Licence';
$string['advanced_dlg:about_loaded'] = 'Loaded plugins';
$string['advanced_dlg:about_plugin'] = 'Plugin';
$string['advanced_dlg:about_plugins'] = 'Plugins';
}
}
- if (isloggedin() && !isguestuser()) {
+ if (core_userfeedback::can_give_feedback()) {
$output .= html_writer::div(
$this->render_from_template('core/userfeedback_footer_link', ['url' => core_userfeedback::make_link()->out(false)])
);
Example context (json):
{ "lang": "en"}
}}
-<div class="alert alert-secondary alert-block fade in alert-dismissible">
- <button type="button" class="close" data-dismiss="alert">×</button>
+<div class="alert alert-secondary alert-block fade in">
<iframe id="campaign-content" class="w-100 border-0"></iframe>
</div>
{{#js}}
</div>
<div class="container">
<form>
- <fieldset class="form-group row">
+ <fieldset class="form-group row flex-column">
<div class="form-check fp-linktype-2">
<label class="form-check-label">
<input class="form-check-input" type="radio">
--- /dev/null
+@core
+Feature: Gathering user feedback
+ In order to facilitate data collection from as broad a sample of Moodle users as possible
+ As Moodle HQ
+ We should add a link within Moodle to a permanent URL on which surveys will be placed
+
+ Scenario: Users should see a feedback link on footer when the feature is enabled
+ Given the following config values are set as admin:
+ | enableuserfeedback | 1 |
+ When I log in as "admin"
+ Then I should see "Give feedback" in the "page-footer" "region"
+
+ Scenario: Users should not see a feedback link on footer when the feature is disabled
+ Given the following config values are set as admin:
+ | enableuserfeedback | 0 |
+ When I log in as "admin"
+ Then I should not see "Give feedback" in the "page-footer" "region"
+
+ Scenario: Visitors should not see a feedback link on footer when they are not logged in
+ Given the following config values are set as admin:
+ | enableuserfeedback | 1 |
+ When I am on site homepage
+ Then I should not see "Give feedback" in the "page-footer" "region"
+
+ @javascript
+ Scenario: Users should not see the notification after they click on the remind me later link
+ Given the following config values are set as admin:
+ | enableuserfeedback | 1 |
+ | userfeedback_nextreminder | 2 |
+ | userfeedback_remindafter | 90 |
+ When I log in as "admin"
+ And I follow "Dashboard" in the user menu
+ And I click on "Remind me later" "link"
+ And I reload the page
+ Then I should not see "Give feedback" in the "region-main" "region"
+ And I should not see "Remind me later" in the "region-main" "region"
use external_single_structure;
use external_warnings;
use context_module;
+use mod_h5pactivity\local\manager;
/**
* This is the external method for getting access information for a h5p activity.
$result = [];
// Return all the available capabilities.
+ $manager = manager::create_from_coursemodule($cm);
$capabilities = load_capability_def('mod_h5pactivity');
foreach ($capabilities as $capname => $capdata) {
$field = 'can' . str_replace('mod/h5pactivity:', '', $capname);
- $result[$field] = has_capability($capname, $context);
+ // For mod/h5pactivity:submit we need to check if tracking is enabled in the h5pactivity for the current user.
+ if ($field == 'cansubmit') {
+ $result[$field] = $manager->is_tracking_enabled();
+ } else {
+ $result[$field] = has_capability($capname, $context);
+ }
}
$result['warnings'] = [];
'coursemodule' => [
'type' => PARAM_INT
],
+ 'context' => [
+ 'type' => PARAM_INT
+ ],
'introfiles' => [
'type' => external_files::get_properties_for_exporter(),
'multiple' => true
$values = [
'coursemodule' => $context->instanceid,
+ 'context' => $context->id,
];
$values['introfiles'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'intro', false, false);
// Set query SQL.
$capjoin = get_enrolled_with_capabilities_join($this->manager->get_context(), '', 'mod/h5pactivity:submit');
$this->set_sql(
- 'u.*',
+ 'DISTINCT u.id, u.picture, u.firstname, u.lastname, u.firstnamephonetic, u.lastnamephonetic,
+ u.middlename, u.alternatename, u.imagealt, u.email',
"{user} u $capjoin->joins",
$capjoin->wheres,
$capjoin->params);
$string['attempts_none'] = 'This user has no attempts to display.';
$string['choice'] = 'Choice';
$string['completion'] = 'Completion';
-$string['contentbank'] = 'more information about the content bank';
-$string['contentbank_help'] = 'Within the content bank you can create and store contents using several authoring
- tools which includes an integrated H5P packacge creator.';
+$string['contentbank'] = 'More information about the content bank';
+$string['contentbank_help'] = 'In the content bank you can create and store content using several authoring tools, including an integrated H5P creator.';
$string['correct_answer'] = 'Correct answer';
$string['deleteallattempts'] = 'Delete all H5P attempts';
$string['displayexport'] = 'Allow download';
$string['displayembed'] = 'Embed button';
$string['displaycopyright'] = 'Copyright button';
+$string['dnduploadh5pactivity'] = 'Add an H5P activity';
$string['duration'] = 'Duration';
$string['enabletracking'] = 'Enable attempt tracking';
$string['false'] = 'False';
0, ['subdirs' => 0, 'maxfiles' => 1]);
}
}
+
+/**
+ * Register the ability to handle drag and drop file uploads
+ * @return array containing details of the files / types the mod can handle
+ */
+function h5pactivity_dndupload_register(): array {
+ return [
+ 'files' => [
+ [
+ 'extension' => 'h5p',
+ 'message' => get_string('dnduploadh5pactivity', 'h5pactivity')
+ ]
+ ]
+ ];
+}
+
+/**
+ * Handle a file that has been uploaded
+ * @param object $uploadinfo details of the file / content that has been uploaded
+ * @return int instance id of the newly created mod
+ */
+function h5pactivity_dndupload_handle($uploadinfo): int {
+ global $CFG;
+
+ $context = context_module::instance($uploadinfo->coursemodule);
+ file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_h5pactivity', 'package', 0);
+ $fs = get_file_storage();
+ $files = $fs->get_area_files($context->id, 'mod_h5pactivity', 'package', 0, 'sortorder, itemid, filepath, filename', false);
+ $file = reset($files);
+
+ // Create a default h5pactivity object to pass to h5pactivity_add_instance()!
+ $h5p = get_config('h5pactivity');
+ $h5p->intro = '';
+ $h5p->introformat = FORMAT_HTML;
+ $h5p->course = $uploadinfo->course->id;
+ $h5p->coursemodule = $uploadinfo->coursemodule;
+ $h5p->grade = $CFG->gradepointdefault;
+
+ // Add some special handling for the H5P options checkboxes.
+ $factory = new \core_h5p\factory();
+ $core = $factory->get_core();
+ if (isset($uploadinfo->displayopt)) {
+ $config = (object) $uploadinfo->displayopt;
+ } else {
+ $config = \core_h5p\helper::decode_display_options($core);
+ }
+ $h5p->displayoptions = \core_h5p\helper::get_display_options($core, $config);
+
+ $h5p->cmidnumber = '';
+ $h5p->name = $uploadinfo->displayname;
+ $h5p->reference = $file->get_filename();
+
+ return h5pactivity_add_instance($h5p, null);
+}
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template mod_h5pactivity/result/answer
+ @template mod_h5pactivity/local/result/answer
This template render all kind of answers/choice in a results table.
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template mod_h5pactivity/result/header
+ @template mod_h5pactivity/local/result/header
This template will render a results header inside mod_h5pactivity results report.
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template mod_h5pactivity/result/options
+ @template mod_h5pactivity/local/result/options
This template will render a choices table inside a H5P activity results report.
{{#options}}
<tr>
<td>{{description}}</td>
- <td>{{#correctanswer}}{{>mod_h5pactivity/result/answer}}{{/correctanswer}}</td>
- <td>{{#useranswer}}{{>mod_h5pactivity/result/answer}}{{/useranswer}}</td>
+ <td>{{#correctanswer}}{{>mod_h5pactivity/local/result/answer}}{{/correctanswer}}</td>
+ <td>{{#useranswer}}{{>mod_h5pactivity/local/result/answer}}{{/useranswer}}</td>
</tr>
{{/options}}
{{#score}}
Variables optional for this template:
* hasoptions - If an option table must be present
* optionslabel - The right label for available options on this result type
- * options - An array of mod_h5pactivity/result/options compatible array
+ * options - An array of mod_h5pactivity/local/result/options compatible array
* content - Extra content in HTML
* track - Indicate if the result has displayable tracking
<div class="container-fluid w-100 my-0 p-0">
<div class="row w-100 py-3 px-1 m-0 p-md-3">
- {{>mod_h5pactivity/result/header}}
+ {{>mod_h5pactivity/local/result/header}}
{{{content}}}
{{#hasoptions}}
- {{>mod_h5pactivity/result/options}}
+ {{>mod_h5pactivity/local/result/options}}
{{/hasoptions}}
{{^track}}
<div class="alert alert-warning w-100" role="alert">
'introformat' => 1
];
$activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params);
- // Add filename to make easier the asserts.
+ // Add filename and contextid to make easier the asserts.
$activities[0]->filename = 'filltheblanks.h5p';
+ $context = context_module::instance($activities[0]->cmid);
+ $activities[0]->contextid = $context->id;
+
$params = [
'course' => $course1->id,
'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/greeting-card-887.h5p',
'introformat' => 1
];
$activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params);
- // Add filename to make easier the asserts.
+ // Add filename and contextid to make easier the asserts.
$activities[1]->filename = 'greeting-card-887.h5p';
+ $context = context_module::instance($activities[1]->cmid);
+ $activities[1]->contextid = $context->id;
$course2 = $this->getDataGenerator()->create_course();
$params = [
];
$activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params);
$activities[2]->filename = 'guess-the-answer.h5p';
-
$context = context_module::instance($activities[2]->cmid);
+ $activities[2]->contextid = $context->id;
+
// Create a fake deploy H5P file.
$generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
$deployedfile = $generator->create_export_file($activities[2]->filename, $context->id, 'mod_h5pactivity', 'package');
$this->assertEquals($activities[$i]->enabletracking, $result['h5pactivities'][$i]['enabletracking']);
$this->assertEquals($activities[$i]->grademethod, $result['h5pactivities'][$i]['grademethod']);
$this->assertEquals($activities[$i]->cmid, $result['h5pactivities'][$i]['coursemodule']);
+ $this->assertEquals($activities[$i]->contextid, $result['h5pactivities'][$i]['context']);
$this->assertEquals($activities[$i]->filename, $result['h5pactivities'][$i]['package'][0]['filename']);
}
}
/**
* Test the behaviour of get_h5pactivity_access_information().
+ *
+ * @dataProvider get_h5pactivity_access_information_data
+ * @param string $role user role in course
+ * @param int $enabletracking if tracking is enabled
+ * @param array $enabledcaps capabilities enabled
*/
- public function test_get_h5pactivity_access_information() {
+ public function test_get_h5pactivity_access_information(string $role, int $enabletracking, array $enabledcaps) {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
- $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
- $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
- $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+ $activity = $this->getDataGenerator()->create_module('h5pactivity',
+ [
+ 'course' => $course,
+ 'enabletracking' => $enabletracking
+ ]
+ );
- // Check the access information for a student.
- $this->setUser($student);
- $result = get_h5pactivity_access_information::execute($activity->id);
- $result = external_api::clean_returnvalue(get_h5pactivity_access_information::execute_returns(), $result);
- $this->assertCount(0, $result['warnings']);
- unset($result['warnings']);
-
- // Check default values for capabilities for student.
- $enabledcaps = ['canview', 'cansubmit'];
- foreach ($result as $capname => $capvalue) {
- if (in_array($capname, $enabledcaps)) {
- $this->assertTrue($capvalue);
- } else {
- $this->assertFalse($capvalue);
- }
+ if ($role) {
+ $user = $this->getDataGenerator()->create_and_enrol($course, $role);
+ $this->setUser($user);
}
- // Check the access information for a teacher.
- $this->setUser($teacher);
+ // Check the access information.
$result = get_h5pactivity_access_information::execute($activity->id);
$result = external_api::clean_returnvalue(get_h5pactivity_access_information::execute_returns(), $result);
$this->assertCount(0, $result['warnings']);
unset($result['warnings']);
- // Check default values for capabilities for teacher.
- $enabledcaps = ['canview', 'canaddinstance', 'canreviewattempts'];
+ // Check the values for capabilities.
foreach ($result as $capname => $capvalue) {
if (in_array($capname, $enabledcaps)) {
$this->assertTrue($capvalue);
$this->assertFalse($capvalue);
}
}
+ }
+
+ /**
+ * Data provider for get_h5pactivity_access_information.
+ *
+ * @return array
+ */
+ public function get_h5pactivity_access_information_data(): array {
+ return [
+ 'Admin, tracking enabled' => [
+ '', 1, ['canview', 'canreviewattempts', 'canaddinstance']
+ ],
+ 'Admin, tracking disabled' => [
+ '', 0, ['canview', 'canreviewattempts', 'canaddinstance']
+ ],
+ 'Student, tracking enabled' => [
+ 'student', 1, ['canview', 'cansubmit']
+ ],
+ 'Student, tracking disabled' => [
+ 'student', 0, ['canview']
+ ],
+ 'Teacher, tracking enabled' => [
+ 'editingteacher', 1, [
+ 'canview',
+ 'canreviewattempts',
+ 'canaddinstance'
+ ]
+ ],
+ 'Teacher, tracking disabled' => [
+ 'editingteacher', 0, [
+ 'canview',
+ 'canreviewattempts',
+ 'canaddinstance'
+ ]
+ ],
+ ];
+ }
+
+ /**
+ * Test dml_missing_record_exception in get_h5pactivity_access_information.
+ */
+ public function test_dml_missing_record_exception() {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $course = $this->getDataGenerator()->create_course();
// Call the WS using an unexisting h5pactivityid.
$this->expectException(dml_missing_record_exception::class);
- $result = get_h5pactivity_access_information::execute($activity->id + 1);
+ $result = get_h5pactivity_access_information::execute(1);
}
}
\ No newline at end of file
display: none;
}
+
.que.calculatedmulti .answer div.r0,
.que.calculatedmulti .answer div.r1 {
- padding: 0.3em;
+ display: flex;
+ margin: 0.25rem 0;
+ align-items: flex-start;
+}
+
+.que.calculatedmulti .answer div.r0 input,
+.que.calculatedmulti .answer div.r1 input {
+ margin: 0.3rem 0.5rem;
+ width: 14px;
}
define(['jquery', 'core/custom_interaction_events'], function($, CustomEvents) {
var SELECTORS = {
- ANSWER_RADIOS: '.answer input',
- CLEARRESULTS_BUTTON: 'button[data-action="clearresults"]'
+ CHOICE_ELEMENT: '.answer input',
+ LINK: 'a',
+ RADIO: 'input[type="radio"]'
};
- var CSSHIDDEN = 'd-none';
+ /**
+ * Mark clear choice radio as enabled and checked.
+ *
+ * @param {Object} clearChoiceContainer The clear choice option container.
+ */
+ var checkClearChoiceRadio = function(clearChoiceContainer) {
+ clearChoiceContainer.find(SELECTORS.RADIO).prop('disabled', false).prop('checked', true);
+ };
+
+ /**
+ * Get the clear choice div container.
+ *
+ * @param {Object} root The question root element.
+ * @param {string} fieldPrefix The question outer div prefix.
+ * @returns {Object} The clear choice div container.
+ */
+ var getClearChoiceElement = function(root, fieldPrefix) {
+ return root.find('div[id="' + fieldPrefix + '"]');
+ };
+
+ /**
+ * Hide clear choice option.
+ *
+ * @param {Object} clearChoiceContainer The clear choice option container.
+ */
+ var hideClearChoiceOption = function(clearChoiceContainer) {
+ clearChoiceContainer.addClass('sr-only');
+ clearChoiceContainer.find(SELECTORS.LINK).attr('tabindex', -1);
+ };
+
+ /**
+ * Shows clear choice option.
+ *
+ * @param {Object} clearChoiceContainer The clear choice option container.
+ */
+ var showClearChoiceOption = function(clearChoiceContainer) {
+ clearChoiceContainer.removeClass('sr-only');
+ clearChoiceContainer.find(SELECTORS.LINK).attr('tabindex', 0);
+ clearChoiceContainer.find(SELECTORS.RADIO).prop('disabled', true);
+ };
/**
* Register event listeners for the clear choice module.
*
* @param {Object} root The question outer div prefix.
+ * @param {string} fieldPrefix The "Clear choice" div prefix.
*/
- var registerEventListeners = function(root) {
+ var registerEventListeners = function(root, fieldPrefix) {
+ var clearChoiceContainer = getClearChoiceElement(root, fieldPrefix);
+
+ clearChoiceContainer.on(CustomEvents.events.activate, SELECTORS.LINK, function(e, data) {
- var clearChoiceButton = root.find(SELECTORS.CLEARRESULTS_BUTTON);
+ // Mark the clear choice radio element as checked.
+ checkClearChoiceRadio(clearChoiceContainer);
+ // Now that the hidden radio has been checked, hide the clear choice option.
+ hideClearChoiceOption(clearChoiceContainer);
+
+ data.originalEvent.preventDefault();
+ });
- root.on(CustomEvents.events.activate, SELECTORS.CLEARRESULTS_BUTTON, function(e, data) {
- root.find(SELECTORS.ANSWER_RADIOS).each(function() {
- $(this).prop('checked', false);
- });
- $(e.target).addClass(CSSHIDDEN);
- data.originalEvent.preventDefault();
+ root.on(CustomEvents.events.activate, SELECTORS.CHOICE_ELEMENT, function() {
+ // If the event has been triggered by any other choice, show the clear choice option.
+ showClearChoiceOption(clearChoiceContainer);
});
- root.on(CustomEvents.events.activate, SELECTORS.ANSWER_RADIOS, function() {
- clearChoiceButton.removeClass(CSSHIDDEN);
+ // If the clear choice radio receives focus from using the tab key, return the focus
+ // to the first answer option.
+ clearChoiceContainer.find(SELECTORS.RADIO).focus(function() {
+ var firstChoice = root.find(SELECTORS.CHOICE_ELEMENT).first();
+ firstChoice.focus();
});
};
* Initialise clear choice module.
* @param {string} root The question outer div prefix.
+ * @param {string} fieldPrefix The "Clear choice" div prefix.
*/
- var init = function(root) {
+ var init = function(root, fieldPrefix) {
root = $('#' + root);
- registerEventListeners(root);
+ registerEventListeners(root, fieldPrefix);
};
return {
}
}
- $questiondivid = $qa->get_outer_question_div_unique_id();
+ $clearchoiceid = $this->get_input_id($qa, -1);
+ $clearchoicefieldname = $qa->get_qt_field_name('clearchoice');
+ $clearchoiceradioattrs = [
+ 'type' => $this->get_input_type(),
+ 'name' => $qa->get_qt_field_name('answer'),
+ 'id' => $clearchoiceid,
+ 'value' => -1,
+ 'class' => 'sr-only'
+ ];
+ $cssclass = 'qtype_multichoice_clearchoice';
// When no choice selected during rendering, then hide the clear choice option.
- $cssclass = '';
+ $linktabindex = 0;
if (!$hascheckedchoice && $response == -1) {
- $cssclass = 'd-none';
+ $cssclass .= ' sr-only';
+ $clearchoiceradioattrs['checked'] = 'checked';
+ $linktabindex = -1;
}
+ // Adds an hidden radio that will be checked to give the impression the choice has been cleared.
+ $clearchoiceradio = html_writer::empty_tag('input', $clearchoiceradioattrs);
+ $clearchoiceradio .= html_writer::link('', get_string('clearchoice', 'qtype_multichoice'),
+ ['for' => $clearchoiceid, 'role' => 'button', 'tabindex' => $linktabindex,
+ 'class' => 'btn btn-link ml-4 pl-1 mt-2']);
- $clearchoicebutton = html_writer::tag('button', get_string('clearchoice', 'qtype_multichoice'), [
- 'class' => 'btn btn-link ml-3 ' . $cssclass,
- 'data-action' => 'clearresults',
- 'data-target' => '#' . $questiondivid
- ]);
+ // Now wrap the radio and label inside a div.
+ $result = html_writer::tag('div', $clearchoiceradio, ['id' => $clearchoicefieldname, 'class' => $cssclass]);
// Load required clearchoice AMD module.
$this->page->requires->js_call_amd('qtype_multichoice/clearchoice', 'init',
- [$questiondivid]);
+ [$qa->get_outer_question_div_unique_id(), $clearchoicefieldname]);
- return $clearchoicebutton;
+ return $result;
}
}
.que.multichoice .answer div.r0 input,
.que.multichoice .answer div.r1 input {
- margin: 0.4rem 0.5rem;
+ margin: 0.3rem 0.5rem;
+ width: 14px;
}
/* Editing form. */
| 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 |
+ | 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 |
Then I should not see "Clear my choice"
And I click on "Check" "button" in the "Question One" "question"
And I should see "Please select an answer" in the "Question One" "question"
+
+ @javascript
+ Scenario: Attempt a quiz and revisit a cleared answer.
+ When I log in as "student1"
+ And I am on "Course 1" course homepage
+ And I follow "Quiz 1"
+ And I press "Attempt quiz now"
+ And I should see "Question One"
+ And I click on "Four" "radio" in the "Question One" "question"
+ And I follow "Finish attempt ..."
+ And I click on "Return to attempt" "button"
+ And I click on "Clear my choice" "button" in the "Question One" "question"
+ And I follow "Finish attempt ..."
+ And I click on "Return to attempt" "button"
+ Then I should not see "Clear my choice"
new question_pattern_expectation('/class="r1"/'));
}
+ /**
+ * Test for clear choice option.
+ */
+ public function test_deferredfeedback_feedback_multichoice_clearchoice() {
+
+ // Create a multichoice, single question.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+ $mc->shuffleanswers = false;
+
+ $clearchoice = -1;
+ $rightchoice = 0;
+ $wrongchoice = 2;
+
+ $this->start_attempt_at_question($mc, 'deferredfeedback', 3);
+
+ // Let's first submit the wrong choice (2).
+ $this->process_submission(array('answer' => $wrongchoice)); // Wrong choice (2).
+
+ $this->check_current_mark(null);
+ // Clear choice radio should not be checked.
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightchoice, true, false), // Not checked.
+ $this->get_contains_mc_radio_expectation($rightchoice + 1, true, false), // Not checked.
+ $this->get_contains_mc_radio_expectation($rightchoice + 2, true, true), // Wrong choice (2) checked.
+ $this->get_contains_mc_radio_expectation($clearchoice, true, false), // Not checked.
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation()
+ );
+
+ // Now, let's clear our previous choice.
+ $this->process_submission(array('answer' => $clearchoice)); // Clear choice (-1).
+ $this->check_current_mark(null);
+
+ // This time, the clear choice radio should be the only one checked.
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightchoice, true, false), // Not checked.
+ $this->get_contains_mc_radio_expectation($rightchoice + 1, true, false), // Not checked.
+ $this->get_contains_mc_radio_expectation($rightchoice + 2, true, false), // Not checked.
+ $this->get_contains_mc_radio_expectation($clearchoice, true, true), // Clear choice radio checked.
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation()
+ );
+
+ // Finally, let's submit the right choice.
+ $this->process_submission(array('answer' => $rightchoice)); // Right choice (0).
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightchoice, true, true),
+ $this->get_contains_mc_radio_expectation($rightchoice + 1, true, false),
+ $this->get_contains_mc_radio_expectation($rightchoice + 2, true, false),
+ $this->get_contains_mc_radio_expectation($clearchoice, true, false),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation()
+ );
+
+ // Finish the attempt.
+ $this->finish();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(3);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightchoice, false, true),
+ $this->get_contains_correct_expectation(),
+ new question_pattern_expectation('/class="r0 correct"/'),
+ new question_pattern_expectation('/class="r1"/'));
+ }
+
public function test_deferredfeedback_feedback_multichoice_multi_showstandardunstruction_yes() {
// Create a multichoice, multi question.
--- /dev/null
+@repository @repository_contentbank @javascript @core_h5p
+Feature: Updating a file in the content bank after using in a course
+ In order to use file alias
+ As a user
+ Updated files must update references when is an alias
+
+ Background:
+ Given the following "categories" exist:
+ | name | category | idnumber |
+ | Category1 | 0 | CAT1 |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course1 | C1 | CAT1 |
+ And the following "contentbank content" exist:
+ | contextlevel | reference | contenttype | user | contentname | filepath |
+ | Course | C1 | contenttype_h5p | admin | package.h5p | /h5p/tests/fixtures/guess-the-answer.h5p |
+ And the following "activities" exist:
+ | activity | name | intro | introformat | course | content | contentformat | idnumber |
+ | page | PageName1 | PageDesc1 | 1 | C1 | H5Ptest | 1 | 1 |
+ And I log in as "admin"
+
+ Scenario: Referenced files updates alias as well
+ Given I am on "Course1" course homepage
+ And I follow "PageName1"
+ And I navigate to "Edit settings" in current page administration
+ And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
+ And I click on "Browse repositories..." "button" in the "Insert H5P" "dialogue"
+ And I select "Content bank" repository in file picker
+ And I click on "package.h5p" "file" in repository content area
+ And I click on "Create an alias/shortcut to the file" "radio"
+ And I click on "Select this file" "button"
+ And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
+ And I wait until the page is ready
+ And I click on "Save and display" "button"
+ And I switch to "h5p-iframe" class iframe
+ And I switch to "h5p-iframe" class iframe
+ And I should see "Press here to reveal answer"
+ And I switch to the main frame
+ # Now edit the content in the content bank.
+ When I am on "Course1" course homepage with editing mode on
+ And I add the "Navigation" block if not present
+ And I expand "Site pages" node
+ And I click on "Content bank" "link"
+ And I click on "package.h5p" "link"
+ And I click on "Edit" "link"
+ And I wait until the page is ready
+ And I switch to "h5p-editor-iframe" class iframe
+ And I set the field "Title" to "Required title"
+ And I set the field "Descriptive solution label" to "This is a new text"
+ And I switch to the main frame
+ And I click on "Save" "button"
+ And I switch to "h5p-player" class iframe
+ And I switch to "h5p-iframe" class iframe
+ And I should see "This is a new text"
+ And I switch to the main frame
+ # Check the course page is updated.
+ Then I am on "Course1" course homepage
+ And I follow "PageName1"
+ And I switch to "h5p-iframe" class iframe
+ And I switch to "h5p-iframe" class iframe
+ And I should see "This is a new text"
+ And I switch to the main frame
+
+ Scenario: Copied files should not be updated if the original is edited
+ Given I am on "Course1" course homepage
+ And I follow "PageName1"
+ And I navigate to "Edit settings" in current page administration
+ And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
+ And I click on "Browse repositories..." "button" in the "Insert H5P" "dialogue"
+ And I select "Content bank" repository in file picker
+ And I click on "package.h5p" "file" in repository content area
+ And I click on "Select this file" "button"
+ And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
+ And I wait until the page is ready
+ And I click on "Save and display" "button"
+ And I switch to "h5p-iframe" class iframe
+ And I switch to "h5p-iframe" class iframe
+ And I should see "Press here to reveal answer"
+ And I switch to the main frame
+ # Now edit the content in the content bank.
+ When I am on "Course1" course homepage with editing mode on
+ And I add the "Navigation" block if not present
+ And I expand "Site pages" node
+ And I click on "Content bank" "link"
+ And I click on "package.h5p" "link"
+ And I click on "Edit" "link"
+ And I wait until the page is ready
+ And I switch to "h5p-editor-iframe" class iframe
+ And I set the field "Title" to "Required title"
+ And I set the field "Descriptive solution label" to "This is a new text"
+ And I switch to the main frame
+ And I click on "Save" "button"
+ And I switch to "h5p-player" class iframe
+ And I switch to "h5p-iframe" class iframe
+ And I should see "This is a new text"
+ And I switch to the main frame
+ # Check the course page is not updated.
+ Then I am on "Course1" course homepage
+ And I follow "PageName1"
+ And I switch to "h5p-iframe" class iframe
+ And I switch to "h5p-iframe" class iframe
+ And I should see "Press here to reveal answer"
+ And I switch to the main frame
$string['fulltext'] = 'Full text';
$string['information'] = '<div>Get a <a href="http://www.flickr.com/services/api/keys/">Flickr API Key</a> for your Moodle site. </div>';
$string['invalidemail'] = 'Invalid email account for flickr';
-$string['license'] = 'License';
+$string['license'] = 'Licence';
$string['modification'] = 'I want to be able to modify the images';
$string['notitle'] = 'notitle';
$string['nullphotolist'] = 'There are no photos in this account';
*/
$string['configplugin'] = 'Merlot.org configuration';
-$string['licensekey'] = 'License key';
+$string['licensekey'] = 'Licence key';
$string['pluginname_help'] = 'Merlot.org';
$string['pluginname'] = 'Merlot.org';
$string['merlot:view'] = 'View the Merlot repository';
}
}
+// Safari does not allow custom styling of checkboxes.
+.safari {
+ input[type="checkbox"],
+ input[type="radio"] {
+ &.focus,
+ &:focus {
+ outline: auto;
+ }
+ }
+}
+
.usermenu,
div.dropdown-item {
a,
button.close:focus:hover {
text-decoration: none; }
+.safari input[type="checkbox"].focus, .safari input[type="checkbox"]:focus,
+.safari input[type="radio"].focus,
+.safari input[type="radio"]:focus {
+ outline: auto; }
+
.usermenu a,
.usermenu a[role="button"],
div.dropdown-item a,
.region-main {
flex: 0 0 100%;
padding: 0 1rem;
+ max-width: 100%;
}
&.blocks-pre {
}
@include media-breakpoint-up(sm) {
- .block_myoverview,
- .block_recentlyaccesseditems {
- .dashboard-card-deck {
- .dashboard-card {
- width: calc(33.33% - #{$card-gutter});
- }
+ .dashboard-card-deck .dashboard-card {
+ width: calc(50% - #{$card-gutter});
+ }
+}
+
+@include media-breakpoint-up(md) {
+ .dashboard-card-deck .dashboard-card {
+ width: calc(50% - #{$card-gutter});
+ }
+ .blocks-post,
+ .blocks-pre {
+ .dashboard-card-deck .dashboard-card {
+ width: calc(100% - #{$card-gutter});
+ }
+ }
+}
+
+@include media-breakpoint-up(lg) {
+ .dashboard-card-deck .dashboard-card {
+ width: calc(33.33% - #{$card-gutter});
+ }
+ .blocks-post,
+ .blocks-pre {
+ .dashboard-card-deck .dashboard-card {
+ width: calc(50% - #{$card-gutter});
+ }
+ }
+}
+
+@include media-breakpoint-up(xl) {
+ .dashboard-card-deck .dashboard-card {
+ width: calc(25% - #{$card-gutter});
+ }
+ .blocks-post,
+ .blocks-pre {
+ .dashboard-card-deck .dashboard-card {
+ width: calc(33.33% - #{$card-gutter});
}
}
}
button.close:focus:hover {
text-decoration: none; }
+.safari input[type="checkbox"].focus, .safari input[type="checkbox"]:focus,
+.safari input[type="radio"].focus,
+.safari input[type="radio"]:focus {
+ outline: auto; }
+
.usermenu a,
.usermenu a[role="button"],
div.dropdown-item a,
display: flex; }
#page-content .region-main {
flex: 0 0 100%;
- padding: 0 1rem; }
+ padding: 0 1rem;
+ max-width: 100%; }
#page-content.blocks-pre .columnleft {
flex: 0 0 32%;
order: -1;
display: flex; }
#page-content .region-main {
flex: 0 0 100%;
- padding: 0 1rem; }
+ padding: 0 1rem;
+ max-width: 100%; }
#page-content.blocks-pre .columnleft {
flex: 0 0 25%;
order: -1;
display: flex; }
#page-content .region-main {
flex: 0 0 100%;
- padding: 0 1rem; }
+ padding: 0 1rem;
+ max-width: 100%; }
#page-content.blocks-pre .columnleft {
flex: 0 0 20%;
order: -1;
/* stylelint-disable-line declaration-no-important */ } }
@media (min-width: 576px) {
- .block_myoverview .dashboard-card-deck .dashboard-card,
- .block_recentlyaccesseditems .dashboard-card-deck .dashboard-card {
+ .dashboard-card-deck .dashboard-card {
+ width: calc(50% - 0.5rem); } }
+
+@media (min-width: 768px) {
+ .dashboard-card-deck .dashboard-card {
+ width: calc(50% - 0.5rem); }
+ .blocks-post .dashboard-card-deck .dashboard-card,
+ .blocks-pre .dashboard-card-deck .dashboard-card {
+ width: calc(100% - 0.5rem); } }
+
+@media (min-width: 992px) {
+ .dashboard-card-deck .dashboard-card {
+ width: calc(33.33% - 0.5rem); }
+ .blocks-post .dashboard-card-deck .dashboard-card,
+ .blocks-pre .dashboard-card-deck .dashboard-card {
+ width: calc(50% - 0.5rem); } }
+
+@media (min-width: 1200px) {
+ .dashboard-card-deck .dashboard-card {
+ width: calc(25% - 0.5rem); }
+ .blocks-post .dashboard-card-deck .dashboard-card,
+ .blocks-pre .dashboard-card-deck .dashboard-card {
width: calc(33.33% - 0.5rem); } }
@media (min-width: 768px) {
}
}
- // Add any supplied additional WHERE clauses.
+ // Add any supplied additional forced WHERE clauses.
if (!empty($additionalwhere)) {
- $wheres[] = $additionalwhere;
+ $innerwhere .= " AND ({$additionalwhere})";
$params = array_merge($params, $additionalparams);
}
And I should not see "Student 2" in the "participants" "table"
And I should not see "Student 3" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
+
+ @javascript
+ Scenario: Initials filtering is always applied in addition to any other filtering
+ Given I log in as "teacher1"
+ And I am on "Course 2" course homepage
+ And I navigate to course participants
+ And I should see "Student 1" in the "participants" "table"
+ And I should see "Student 2" in the "participants" "table"
+ And I should see "Student 3" in the "participants" "table"
+ And I should see "Trendy Learnson" in the "participants" "table"
+ And I should see "Teacher 1" in the "participants" "table"
+ When I set the field "Match" in the "Filter 1" "fieldset" to "Any"
+ And I set the field "type" in the "Filter 1" "fieldset" to "Role"
+ And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
+ And I click on "Student" "list_item"
+ And I click on "Apply filters" "button"
+ When I click on "T" "link" in the ".firstinitial" "css_element"
+ Then I should see "Trendy Learnson" in the "participants" "table"
+ And I should not see "Student 1" in the "participants" "table"
+ And I should not see "Student 2" in the "participants" "table"
+ And I should not see "Student 3" in the "participants" "table"
+ And I should not see "Teacher 1" in the "participants" "table"
defined('MOODLE_INTERNAL') || die();
-$version = 2020060700.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2020060900.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '3.9beta (Build: 20200607)'; // Human-friendly version name
+$release = '3.9rc1 (Build: 20200609)'; // Human-friendly version name
$branch = '39'; // This version's branch.
-$maturity = MATURITY_BETA; // This version's maturity level.
+$maturity = MATURITY_RC; // This version's maturity level.