$string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically. Step definitions available for use in .feature files are listed below.';
$string['allavailablesteps'] = 'All available step definitions';
-$string['errorapproot'] = '$CFG->behat_ionic_dirroot is not pointing to a valid Moodle Mobile developer install.';
+$string['errorapproot'] = '$CFG->behat_ionic_dirroot is not pointing to a valid Moodle app developer install.';
$string['errorbehatcommand'] = 'Error running behat CLI command. Try running "{$a} --help" manually from CLI to find out more about the problem.';
$string['errorcomposer'] = 'Composer dependencies are not installed.';
$string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
And I press "Add category"
And I set the field "Name" to "Category 1"
And I set the field "Description" to "Category 1 description"
- When I click on "Save" "button" in the "Delete category" "dialogue"
+ When I click on "Save" "button" in the "Add category" "dialogue"
Then I should see "Category 1" in the "List of data categories" "table"
And I should see "Category 1 description" in the "Category 1" "table_row"
And I choose "Delete" in the open action menu
And I should see "Delete category"
And I should see "Are you sure you want to delete the category 'Category 1'?"
- When I click on "Delete" "button" in the "Confirm" "dialogue"
+ When I click on "Delete" "button" in the "Delete category" "dialogue"
Then I should not see "Category 1" in the "List of data categories" "table"
And I choose "Delete" in the open action menu
And I should see "Delete purpose"
And I should see "Are you sure you want to delete the purpose 'Purpose 1'?"
- When I click on "Delete" "button" in the "Confirm" "dialogue"
+ When I click on "Delete" "button" in the "Delete purpose" "dialogue"
Then I should not see "Purpose 1" in the "List of data purposes" "table"
templates.render('tool_usertours/tourstep', {})
)
.then(function(response, template) {
+ // If we don't have any tour config (because it doesn't need showing for the current user), return early.
+ if (!response.hasOwnProperty('tourconfig')) {
+ return;
+ }
+
return usertours.startBootstrapTour(tourId, template[0], response.tourconfig);
})
.always(function() {
\tool_usertours\event\tour_started::create([
'contextid' => $context->id,
- 'objectid' => $tourid,
+ 'objectid' => $tour->get_id(),
'other' => [
- 'pageurl' => $pageurl,
+ 'pageurl' => $params['pageurl'],
],
])->trigger();
'tourconfig' => new external_single_structure([
'name' => new external_value(PARAM_RAW, 'Tour Name'),
'steps' => new external_multiple_structure(self::step_structure_returns()),
- ])
+ ], 'Tour config', VALUE_OPTIONAL)
]);
}
This files describes API changes in the tool_usertours code.
+=== 3.9 ===
+* The `tourconfig` property returned by the `tool_usertours_fetch_and_start_tour`
+ external method is now optional, and will be omitted if the tour shouldn't be
+ shown to the current user
+
=== 3.5 ===
* Third party library Popper.js was moved from this plugin into core (core/popper)
Scenario: Delete model
When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Delete" in the open action menu
- And I click on "Delete" "button" in the "Confirm" "dialogue"
+ And I click on "Delete" "button" in the "Delete" "dialogue"
Then I should not see "Students at risk of not meeting the course completion conditions"
$this->assertEquals(print_password_policy(), $result['passwordpolicy']);
$this->assertNotContains('recaptchachallengehash', $result);
$this->assertNotContains('recaptchachallengeimage', $result);
- $this->assertCount(2, $result['profilefields']);
- $this->assertEquals('text', $result['profilefields'][0]['datatype']);
- $this->assertEquals('textarea', $result['profilefields'][1]['datatype']);
+
+ // Whip up a array with named entries to easily check against.
+ $namedarray = array();
+ foreach ($result['profilefields'] as $key => $value) {
+ $namedarray[$value['shortname']] = array(
+ 'datatype' => $value['datatype']
+ );
+ }
+
+ // Just check if we have the fields from this test. If a plugin adds fields we'll let it slide.
+ $this->assertArrayHasKey('frogname', $namedarray);
+ $this->assertArrayHasKey('sometext', $namedarray);
+
+ $this->assertEquals('text', $namedarray['frogname']['datatype']);
+ $this->assertEquals('textarea', $namedarray['sometext']['datatype']);
}
public function test_signup_user() {
And I click on "Grade" "link" in the "s@example.com" "table_row"
And I set the field "Grade out of 100" to "40"
And I click on "Save changes" "button"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
# Log back in as student.
$mform->setType('imagecaption', PARAM_TEXT);
$mform->addHelpButton('imagecaption', 'imagecaption', 'badges');
- $mform->addElement('header', 'issuerdetails', get_string('issuerdetails', 'badges'));
- if (badges_open_badges_backpack_api() != OPEN_BADGES_V2) {
+ if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
+ $mform->addElement('header', 'issuerdetails', get_string('issuerdetails', 'badges'));
+
$mform->addElement('text', 'issuername', get_string('name'), array('size' => '70'));
$mform->setType('issuername', PARAM_NOTAGS);
$mform->addRule('issuername', null, 'required');
$url = parse_url($CFG->wwwroot);
$mform->addElement('hidden', 'issuerurl', $url['scheme'] . '://' . $url['host']);
$mform->setType('issuerurl', PARAM_URL);
-
- } else {
- $name = $CFG->badges_defaultissuername;
- $mform->addElement('static', 'issuernamelabel', get_string('name'), $name);
- $mform->addElement('hidden', 'issuername', $name);
- $mform->setType('issuername', PARAM_NOTAGS);
-
- $contact = $CFG->badges_defaultissuercontact;
- $mform->addElement('static', 'issuercontactlabel', get_string('contact', 'badges'), $contact);
- $mform->addElement('hidden', 'issuercontact', $contact);
- $mform->setType('issuercontact', PARAM_RAW);
-
- $url = parse_url($CFG->wwwroot);
- $mform->addElement('hidden', 'issuerurl', $url['scheme'] . '://' . $url['host']);
- $mform->setType('issuerurl', PARAM_URL);
}
$mform->addElement('header', 'issuancedetails', get_string('issuancedetails', 'badges'));
global $DB;
$errors = parent::validation($data, $files);
- if (badges_open_badges_backpack_api() != OPEN_BADGES_V2) {
+ if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
if (!empty($data['issuercontact']) && !validate_email($data['issuercontact'])) {
$errors['issuercontact'] = get_string('invalidemail');
}
$fordb->usercreated = $USER->id;
$fordb->usermodified = $USER->id;
- if (badges_open_badges_backpack_api() != OPEN_BADGES_V2) {
+ if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
$fordb->issuername = $data->issuername;
$fordb->issuerurl = $data->issuerurl;
$fordb->issuercontact = $data->issuercontact;
Given I am on homepage
And I log in as "admin"
- @javascript
- Scenario: Setting badges settings
- Given I navigate to "Badges > Badges settings" in site administration
- And I set the field "Badge issuer name" to "Test Badge Site"
- And I set the field "Badge issuer email address" to "testuser@example.com"
- And I press "Save changes"
- And I follow "Badges"
- When I follow "Add a new badge"
- And I press "Issuer details"
- Then I should see "testuser@example.com"
- And I should see "Test Badge Site"
-
@javascript
Scenario: Accessing the badges
And I press "Customise this page"
@javascript @_file_upload
Scenario: Add a badge
- Given I navigate to "Badges > Add a new badge" in site administration
+ Given I navigate to "Badges > Badges settings" in site administration
+ And I set the field "Badge issuer name" to "Test Badge Site"
+ And I set the field "Badge issuer email address" to "testuser@example.com"
+ And I press "Save changes"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Test badge with 'apostrophe' and other friends (<>&@#) |
| Version | v1 |
And I should see "Related badges (0)"
And I should see "Alignments (0)"
And I should not see "Create badge"
+ And I should not see "Issuer details"
+ And I follow "Overview"
+ And I should see "Issuer details"
+ And I should see "Test Badge Site"
+ And I should see "testuser@example.com"
And I follow "Manage badges"
And I should see "Number of badges available: 1"
And I should not see "There are no badges available."
And the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
+ And I log in as "admin"
+ And I navigate to "Badges > Badges settings" in site administration
+ And I set the field "Badge issuer name" to "Test Badge Site"
+ And I set the field "Badge issuer email address" to "testuser@example.com"
+ And I log out
@javascript
Scenario: Verify backback settings
| Description | Test badge description |
| Image author | http://author.example.com |
| Image caption | Test caption image |
- | issuername | Test Badge Site |
- | issuercontact | testuser@example.com |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I set the field "type" to "Manual issue by role"
| Description | Test badge description |
| Image author | http://author.example.com |
| Image caption | Test caption image |
- | issuername | Test Badge Site |
- | issuercontact | testuser@example.com |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I set the field "type" to "Manual issue by role"
<div>{{{shortname}}}</div>
{{/showshortname}}
</div>
- <a href="{{viewurl}}" class="coursename">
+ <a href="{{viewurl}}" class="aalink coursename">
{{> core_course/favouriteicon }}
<span class="sr-only">
{{#str}}aria:coursename, core_course{{/str}}
{{/showshortname}}
</div>
<div class="d-flex mb-1">
- <a href="{{viewurl}}" class="coursename">
+ <a href="{{viewurl}}" class="aalink coursename">
{{> core_course/favouriteicon }}
<span class="sr-only">
{{#str}}aria:coursename, core_course{{/str}}
return Templates.replaceNode(target, html, js);
})
.then(() => {
- document.querySelector('body').dispatchEvent(new Event(CalendarEvents.viewUpdated));
+ document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));
return;
})
.always(() => {
return Templates.replaceNode(target, html, js);
})
.then(() => {
- document.querySelector('body').dispatchEvent(new Event(CalendarEvents.viewUpdated));
+ document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));
return;
})
.always(() => {
return Templates.replaceNode(target, html, js);
})
.then(() => {
- document.querySelector('body').dispatchEvent(new Event(CalendarEvents.viewUpdated));
+ document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated));
return;
})
.always(function() {
{
}
}}
-<div class="header d-flex flex-wrap">
+<div class="header d-flex flex-wrap p-1">
{{> core_calendar/view_selector}}
{{#filter_selector}}
{{{filter_selector}}}
data-new-event-timestamp="{{neweventtimestamp}}">
<div class="d-none d-md-block hidden-phone text-xs-center">
{{#hasevents}}
- <a data-action="view-day-link" href="#" class="day" aria-label="{{viewdaylinktitle}}"
+ <a data-action="view-day-link" href="#" class="aalink day" aria-label="{{viewdaylinktitle}}"
data-year="{{date.year}}" data-month="{{date.mon}}" data-day="{{mday}}"
data-courseid="{{courseid}}" data-categoryid="{{categoryid}}"
data-timestamp="{{timestamp}}">{{mday}}</a>
</div>
<div class="d-md-none hidden-desktop hidden-tablet">
{{#hasevents}}
- <a data-action="view-day-link" href="#" class="day" aria-label="{{viewdaylinktitle}}"
+ <a data-action="view-day-link" href="#" class="day aalink" aria-label="{{viewdaylinktitle}}"
data-year="{{date.year}}" data-month="{{date.mon}}" data-day="{{mday}}"
data-courseid="{{courseid}}" data-categoryid="{{categoryid}}"
data-timestamp="{{timestamp}}">{{mday}}</a>
And I set the following fields to these values:
| Grade | 21 |
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I follow "Edit settings"
And I log out
And I log in as "student1"
And I set the following fields to these values:
| Grade | 21 |
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I follow "Edit settings"
And I log out
And I log in as "student1"
// Example:
// define('BEHAT_DISABLE_HISTOGRAM', true);
//
-// Mobile app Behat testing requires this option, pointing to a developer Moodle Mobile directory:
-// $CFG->behat_ionic_dirroot = '/where/I/keep/my/git/checkouts/moodlemobile2';
+// Mobile app Behat testing requires this option, pointing to a developer Moodle app directory:
+// $CFG->behat_ionic_dirroot = '/where/I/keep/my/git/checkouts/moodleapp';
//
// The following option can be used to indicate a running Ionic server (otherwise Behat will start
// one automatically for each test run, which is convenient but takes ages):
namespace core_contentbank;
+use core_plugin_manager;
use stored_file;
use context;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class contentbank {
+ /** @var array Enabled content types. */
+ private $enabledcontenttypes = null;
/**
* Obtains the list of core_contentbank_content objects currently active.
* @return string[] Array of contentbank contenttypes.
*/
public function get_enabled_content_types(): array {
+ if (!is_null($this->enabledcontenttypes)) {
+ return $this->enabledcontenttypes;
+ }
+
$enabledtypes = \core\plugininfo\contenttype::get_enabled_plugins();
$types = [];
foreach ($enabledtypes as $name) {
$contenttypeclassname = "\\contenttype_$name\\contenttype";
$contentclassname = "\\contenttype_$name\\content";
if (class_exists($contenttypeclassname) && class_exists($contentclassname)) {
- $types[] = $name;
+ $types[$contenttypeclassname] = $name;
}
}
- return $types;
+ return $this->enabledcontenttypes = $types;
}
/**
}
return $result;
}
+
+ /**
+ * Get the list of content types that have the requested feature.
+ *
+ * @param string $feature Feature code e.g CAN_UPLOAD.
+ * @param null|\context $context Optional context to check the permission to use the feature.
+ * @param bool $enabled Whether check only the enabled content types or all of them.
+ *
+ * @return string[] List of content types where the user has permission to access the feature.
+ */
+ public function get_contenttypes_with_capability_feature(string $feature, \context $context = null, bool $enabled = true): array {
+ $contenttypes = [];
+ // Check enabled content types or all of them.
+ if ($enabled) {
+ $contenttypestocheck = $this->get_enabled_content_types();
+ } else {
+ $plugins = core_plugin_manager::instance()->get_plugins_of_type('contenttype');
+ foreach ($plugins as $plugin) {
+ $contenttypeclassname = "\\{$plugin->type}_{$plugin->name}\\contenttype";
+ $contenttypestocheck[$contenttypeclassname] = $plugin->name;
+ }
+ }
+
+ foreach ($contenttypestocheck as $classname => $name) {
+ $contenttype = new $classname($context);
+ // The method names that check the features permissions must follow the pattern can_feature.
+ if ($contenttype->{"can_$feature"}()) {
+ $contenttypes[$classname] = $name;
+ }
+ }
+
+ return $contenttypes;
+ }
}
/** Plugin implements uploading feature */
const CAN_UPLOAD = 'upload';
- /** @var context This contenttype's context. **/
+ /** Plugin implements edition feature */
+ const CAN_EDIT = 'edit';
+
+ /** @var \context This contenttype's context. **/
protected $context = null;
/**
/**
* Fills content_bank table with appropiate information.
*
- * @param stdClass $record An optional content record compatible object (default null)
+ * @param \stdClass $record An optional content record compatible object (default null)
* @return content Object with content bank information.
*/
public function create_content(\stdClass $record = null): ?content {
* This method can be overwritten by the plugins if they need to change some other specific information.
*
* @param content $content The content to rename.
- * @param string $name The name of the content.
+ * @param string $name The name of the content.
* @return boolean true if the content has been renamed; false otherwise.
*/
public function rename_content(content $content, string $name): bool {
* This method can be overwritten by the plugins if they need to change some other specific information.
*
* @param content $content The content to rename.
- * @param context $context The new context.
+ * @param \context $context The new context.
* @return boolean true if the content has been renamed; false otherwise.
*/
public function move_content(content $content, \context $context): bool {
return true;
}
+ /**
+ * Returns whether or not the user has permission to use the editor.
+ *
+ * @return bool True if the user can edit content. False otherwise.
+ */
+ final public function can_edit(): bool {
+ if (!$this->is_feature_supported(self::CAN_EDIT)) {
+ return false;
+ }
+
+ if (!$this->can_access()) {
+ 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();
+ }
+
+ /**
+ * Returns plugin allows edition.
+ *
+ * @return bool True if plugin allows edition. False otherwise.
+ */
+ protected function is_edit_allowed(): bool {
+ // Plugins can overwrite this function to add any check they need.
+ return true;
+ }
+
/**
* Returns the plugin supports the feature.
*
* @return array
*/
abstract public function get_manageable_extensions(): array;
+
+ /**
+ * Returns the list of different types of the given content type.
+ *
+ * A content type can have one or more options for creating content. This method will report all of them or only the content
+ * type itself if it has no other options.
+ *
+ * @return array An object for each type:
+ * - string typename: descriptive name of the type.
+ * - string typeeditorparams: params required by this content type editor.
+ * - url typeicon: this type icon.
+ */
+ abstract public function get_contenttype_types(): array;
}
--- /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/>.
+
+/**
+ * Provides {@see \core_contentbank\form\edit_content} class.
+ *
+ * @package core_contentbank
+ * @copyright 2020 Victor Deniz <victor@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank\form;
+
+use moodleform;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Defines the form for editing a content.
+ *
+ * @package core_contentbank
+ * @copyright 2020 Victor Deniz <victor@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class edit_content extends moodleform {
+
+ /** @var int Context the content belongs to. */
+ protected $contextid;
+
+ /** @var string Content type plugin name. */
+ protected $plugin;
+
+ /** @var int Content id in the content bank. */
+ protected $id;
+
+ /**
+ * Constructor.
+ *
+ * @param string $action The action attribute for the form.
+ * @param array $customdata Data to set during instance creation.
+ * @param string $method Form method.
+ */
+ public function __construct(string $action = null, array $customdata = null, string $method = 'post') {
+ parent::__construct($action, $customdata, $method);
+ $this->contextid = $customdata['contextid'];
+ $this->plugin = $customdata['plugin'];
+ $this->id = $customdata['id'];
+
+ $mform =& $this->_form;
+ $mform->addElement('hidden', 'contextid', $this->contextid);
+ $this->_form->setType('contextid', PARAM_INT);
+
+ $mform->addElement('hidden', 'plugin', $this->plugin);
+ $this->_form->setType('plugin', PARAM_PLUGIN);
+
+ $mform->addElement('hidden', 'id', $this->id);
+ $this->_form->setType('id', PARAM_INT);
+ }
+
+ /**
+ * Overrides formslib's add_action_buttons() method.
+ *
+ *
+ * @param bool $cancel
+ * @param string|null $submitlabel
+ *
+ * @return void
+ */
+ public function add_action_buttons($cancel = true, $submitlabel = null): void {
+ if (is_null($submitlabel)) {
+ $submitlabel = get_string('save');
+ }
+ parent::add_action_buttons($cancel, $submitlabel);
+ }
+}
);
}
$data->contents = $contentdata;
- $data->tools = $this->toolbar;
+ // The tools are displayed in the action bar on the index page.
+ foreach ($this->toolbar as $tool) {
+ // Customize the output of a tool, like dropdowns.
+ $method = 'export_tool_'.$tool['name'];
+ if (method_exists($this, $method)) {
+ $this->$method($tool);
+ }
+ $data->tools[] = $tool;
+ }
+
return $data;
}
+
+ /**
+ * Adds the content type items to display to the Add dropdown.
+ *
+ * Each content type is represented as an object with the properties:
+ * - name: the name of the content type.
+ * - baseurl: the base content type editor URL.
+ * - types: different types of the content type to display as dropdown items.
+ *
+ * @param array $tool Data for rendering the Add dropdown, including the editable content types.
+ */
+ private function export_tool_add(array &$tool) {
+ $editabletypes = $tool['contenttypes'];
+
+ $addoptions = [];
+ foreach ($editabletypes as $class => $type) {
+ $contentype = new $class($this->context);
+ // Get the creation options of each content type.
+ $types = $contentype->get_contenttype_types();
+ if ($types) {
+ // Add a text describing the content type as first option. This will be displayed in the drop down to
+ // separate the options for the different content types.
+ $contentdesc = new stdClass();
+ $contentdesc->typename = get_string('description', $contentype->get_contenttype_name());
+ array_unshift($types, $contentdesc);
+ // Context data for the template.
+ $addcontenttype = new stdClass();
+ // Content type name.
+ $addcontenttype->name = $type;
+ // Content type editor base URL.
+ $tool['link']->param('plugin', $type);
+ $addcontenttype->baseurl = $tool['link']->out();
+ // Different types of the content type.
+ $addcontenttype->types = $types;
+ $addoptions[] = $addcontenttype;
+ }
+ }
+
+ $tool['contenttypes'] = $addoptions;
+ }
}
--- /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/>.
+
+/**
+ * Class containing data for a content view.
+ *
+ * @package core_contentbank
+ * @copyright 2020 Victor Deniz <victor@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank\output;
+
+use core_contentbank\content;
+use core_contentbank\contenttype;
+use moodle_url;
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+
+/**
+ * Class containing data for the content view.
+ *
+ * @copyright 2020 Victor Deniz <victor@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class viewcontent implements renderable, templatable {
+ /**
+ * @var contenttype Content bank content type.
+ */
+ private $contenttype;
+
+ /**
+ * @var stdClass Record of the contentbank_content table.
+ */
+ private $content;
+
+ /**
+ * Construct this renderable.
+ *
+ * @param contenttype $contenttype Content bank content type.
+ * @param content $content Record of the contentbank_content table.
+ */
+ public function __construct(contenttype $contenttype, content $content) {
+ $this->contenttype = $contenttype;
+ $this->content = $content;
+ }
+
+ /**
+ * Export this data so it can be used as the context for a mustache template.
+ *
+ * @param renderer_base $output
+ *
+ * @return stdClass
+ */
+ public function export_for_template(renderer_base $output): stdClass {
+ $data = new stdClass();
+
+ // Get the content type html.
+ $contenthtml = $this->contenttype->get_view_content($this->content);
+ $data->contenthtml = $contenthtml;
+
+ // Check if the user can edit this content type.
+ if ($this->contenttype->can_edit()) {
+ $data->usercanedit = true;
+ $urlparams = [
+ 'contextid' => $this->content->get_contextid(),
+ 'plugin' => $this->contenttype->get_plugin_name(),
+ 'id' => $this->content->get_id()
+ ];
+ $editcontenturl = new moodle_url('/contentbank/edit.php', $urlparams);
+ $data->editcontenturl = $editcontenturl->out(false);
+ }
+
+ $closeurl = new moodle_url('/contentbank/index.php', ['contextid' => $this->content->get_contextid()]);
+ $data->closeurl = $closeurl->out(false);
+
+ return $data;
+ }
+}
namespace contenttype_h5p;
use core\event\contentbank_content_viewed;
-use html_writer;
+use stdClass;
+use core_h5p\editor_ajax;
+use core_h5p\file_storage;
+use core_h5p\local\library\autoloader;
+use H5PCore;
/**
* H5P content bank manager class
$event->trigger();
$fileurl = $content->get_file_url();
- $html = html_writer::tag('h2', $content->get_name());
- $html .= \core_h5p\player::display($fileurl, new \stdClass(), true);
+ $html = \core_h5p\player::display($fileurl, new \stdClass(), true);
return $html;
}
* @return array
*/
protected function get_implemented_features(): array {
- return [self::CAN_UPLOAD];
+ return [self::CAN_UPLOAD, self::CAN_EDIT];
}
/**
protected function is_access_allowed(): bool {
return true;
}
+
+ /**
+ * Returns the list of different H5P content types the user can create.
+ *
+ * @return array An object for each H5P content type:
+ * - string typename: descriptive name of the H5P content type.
+ * - string typeeditorparams: params required by the H5P editor.
+ * - url typeicon: H5P content type icon.
+ */
+ public function get_contenttype_types(): array {
+ // Get the H5P content types available.
+ autoloader::register();
+ $editorajax = new editor_ajax();
+ $h5pcontenttypes = $editorajax->getLatestLibraryVersions();
+
+ $types = [];
+ $h5pfilestorage = new file_storage();
+ foreach ($h5pcontenttypes as $h5pcontenttype) {
+ $library = [
+ 'name' => $h5pcontenttype->machine_name,
+ 'majorVersion' => $h5pcontenttype->major_version,
+ 'minorVersion' => $h5pcontenttype->minor_version,
+ ];
+ $key = H5PCore::libraryToString($library);
+ $type = new stdClass();
+ $type->key = $key;
+ $type->typename = $h5pcontenttype->title;
+ $type->typeeditorparams = 'library=' . $key;
+ $type->typeicon = $h5pfilestorage->get_icon_url(
+ $h5pcontenttype->id,
+ $h5pcontenttype->machine_name,
+ $h5pcontenttype->major_version,
+ $h5pcontenttype->minor_version);
+ $types[] = $type;
+ }
+
+ return $types;
+ }
}
--- /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/>.
+
+/**
+ * Provides the class that defines the form for the H5P authoring tool.
+ *
+ * @package contenttype_h5p
+ * @copyright 2020 Victor Deniz <victor@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace contenttype_h5p\form;
+
+use contenttype_h5p\content;
+use contenttype_h5p\contenttype;
+use core_contentbank\form\edit_content;
+use core_h5p\api;
+use core_h5p\editor as h5peditor;
+use core_h5p\factory;
+use stdClass;
+
+/**
+ * Defines the form for editing an H5P content.
+ *
+ * @copyright 2020 Victor Deniz <victor@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class editor extends edit_content {
+
+ /** @var $h5peditor H5P editor object */
+ private $h5peditor;
+
+ /** @var $content The content being edited */
+ private $content;
+
+ /**
+ * Defines the form fields.
+ */
+ protected function definition() {
+ global $DB;
+
+ $mform = $this->_form;
+
+ // Id of the content to edit.
+ $id = $this->_customdata['id'];
+ // H5P content type to create.
+ $library = optional_param('library', null, PARAM_TEXT);
+
+ if (empty($id) && empty($library)) {
+ $returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $this->_customdata['contextid']]);
+ print_error('invalidcontentid', 'error', $returnurl);
+ }
+
+ $this->h5peditor = new h5peditor();
+
+ if ($id) {
+ // The H5P editor needs the H5P content id (h5p table).
+ $record = $DB->get_record('contentbank_content', ['id' => $id]);
+ $this->content = new content($record);
+ $file = $this->content->get_file();
+
+ $h5p = api::get_content_from_pathnamehash($file->get_pathnamehash());
+ $mform->addElement('hidden', 'h5pid', $h5p->id);
+ $mform->setType('h5pid', PARAM_INT);
+ $this->h5peditor->set_content($h5p->id);
+ } else {
+ // The H5P editor needs the H5P content type library name for a new content.
+ $mform->addElement('hidden', 'library', $library);
+ $mform->setType('library', PARAM_TEXT);
+ $this->h5peditor->set_library($library, $this->_customdata['contextid'], 'contentbank', 'public');
+ }
+
+ $mformid = 'coolh5peditor';
+ $mform->setAttributes(array('id' => $mformid) + $mform->getAttributes());
+
+ $this->add_action_buttons();
+
+ $this->h5peditor->add_editor_to_form($mform);
+
+ $this->add_action_buttons();
+ }
+
+ /**
+ * Modify or create an H5P content from the form data.
+ *
+ * @param stdClass $data Form data to create or modify an H5P content.
+ *
+ * @return int The id of the edited or created content.
+ */
+ public function save_content(stdClass $data): int {
+ global $DB;
+
+ // The H5P libraries expect data->id as the H5P content id.
+ // The method \H5PCore::saveContent throws an error if id is set but empty.
+ if (empty($data->id)) {
+ unset($data->id);
+ } else {
+ // The H5P libraries save in $data->id the H5P content id (h5p table), so the content id is saved in another var.
+ $contentid = $data->id;
+ }
+
+ $h5pcontentid = $this->h5peditor->save_content($data);
+
+ $factory = new factory();
+ $h5pfs = $factory->get_framework();
+
+ // Needs the H5P file id to create or update the content bank record.
+ $h5pcontent = $h5pfs->loadContent($h5pcontentid);
+ $fs = get_file_storage();
+ $file = $fs->get_file_by_hash($h5pcontent['pathnamehash']);
+ // Creating new content.
+ if (!isset($data->h5pid)) {
+ // The initial name of the content is the title of the H5P content.
+ $cbrecord = new stdClass();
+ $cbrecord->name = json_decode($data->h5pparams)->metadata->title;
+ $context = \context::instance_by_id($data->contextid, MUST_EXIST);
+ // Create entry in content bank.
+ $contenttype = new contenttype($context);
+ $newcontent = $contenttype->create_content($cbrecord);
+ if ($file && $newcontent) {
+ $updatedfilerecord = new stdClass();
+ $updatedfilerecord->id = $file->get_id();
+ $updatedfilerecord->itemid = $newcontent->get_id();
+ // As itemid changed, the pathnamehash has to be updated in the file table.
+ $pathnamehash = \file_storage::get_pathname_hash($file->get_contextid(), $file->get_component(),
+ $file->get_filearea(), $updatedfilerecord->itemid, $file->get_filepath(), $file->get_filename());
+ $updatedfilerecord->pathnamehash = $pathnamehash;
+ $DB->update_record('files', $updatedfilerecord);
+ // The pathnamehash in the h5p table must match the file pathnamehash.
+ $h5pfs->updateContentFields($h5pcontentid, ['pathnamehash' => $pathnamehash]);
+ }
+ } else {
+ // Update content.
+ $this->content->update_content();
+ }
+
+ return $contentid ?? $newcontent->get_id();
+ }
+}
'editingteacher' => CAP_ALLOW,
]
],
+ 'contenttype/h5p:useeditor' => [
+ 'riskbitmask' => RISK_SPAM,
+ 'captype' => 'write',
+ 'contextlevel' => CONTEXT_COURSE,
+ 'archetypes' => [
+ 'manager' => CAP_ALLOW,
+ 'coursecreator' => CAP_ALLOW,
+ 'editingteacher' => CAP_ALLOW,
+ ]
+ ],
];
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+$string['description'] = 'H5P Interactive Content';
$string['pluginname'] = 'H5P';
$string['pluginname_help'] = 'Content bank to upload and share H5P content';
$string['privacy:metadata'] = 'The H5P content bank plugin does not store any personal data.';
$string['h5p:access'] = 'Access H5P content in the content bank';
$string['h5p:upload'] = 'Upload new H5P content';
+$string['h5p:useeditor'] = 'Create or edit content using the H5P editor';
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2020041500.00; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version = 2020051500.01; // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires = 2020041500.00; // Requires this Moodle version
$plugin->component = 'contenttype_h5p'; // Full name of the plugin (used for diagnostics).
--- /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/>.
+
+/**
+ * Create or update contents through the specific content type editor
+ *
+ * @package core_contentbank
+ * @copyright 2020 Victor Deniz <victor@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require('../config.php');
+
+require_login();
+
+$contextid = required_param('contextid', PARAM_INT);
+$pluginname = required_param('plugin', PARAM_PLUGIN);
+$id = optional_param('id', null, PARAM_INT);
+$context = context::instance_by_id($contextid, MUST_EXIST);
+require_capability('moodle/contentbank:access', $context);
+
+$returnurl = new \moodle_url('/contentbank/view.php', ['id' => $id]);
+
+if (!empty($id)) {
+ $record = $DB->get_record('contentbank_content', ['id' => $id], '*', MUST_EXIST);
+ $contentclass = "$record->contenttype\\content";
+ $content = new $contentclass($record);
+ // Set the heading title.
+ $heading = $content->get_name();
+ // The content type of the content overwrites the pluginname param value.
+ $contenttypename = $content->get_content_type();
+} else {
+ $contenttypename = "contenttype_$pluginname";
+ $heading = get_string('addinganew', 'moodle', get_string('description', $contenttypename));
+}
+
+// Check plugin is enabled.
+$plugin = core_plugin_manager::instance()->get_plugin_info($contenttypename);
+if (!$plugin || !$plugin->is_enabled()) {
+ print_error('unsupported', 'core_contentbank', $returnurl);
+}
+
+// Create content type instance.
+$contenttypeclass = "$contenttypename\\contenttype";
+if (class_exists($contenttypeclass)) {
+ $contenttype = new $contenttypeclass($context);
+} else {
+ 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());
+}
+
+$values = [
+ 'contextid' => $contextid,
+ 'plugin' => $pluginname,
+ 'id' => $id
+];
+
+$title = get_string('contentbank');
+\core_contentbank\helper::get_page_ready($context, $title, true);
+if ($PAGE->course) {
+ require_login($PAGE->course->id);
+}
+
+$PAGE->set_url(new \moodle_url('/contentbank/edit.php', $values));
+$PAGE->set_context($context);
+$PAGE->navbar->add(get_string('edit'));
+$PAGE->set_title($title);
+
+$PAGE->set_heading($heading);
+
+// Instantiate the content type form.
+$editorclass = "$contenttypename\\form\\editor";
+if (!class_exists($editorclass)) {
+ print_error('noformdesc');
+}
+
+$editorform = new $editorclass(null, $values);
+
+if ($editorform->is_cancelled()) {
+ if (empty($id)) {
+ $returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id]);
+ }
+ redirect($returnurl);
+} else if ($data = $editorform->get_data()) {
+ $id = $editorform->save_content($data);
+ // Just in case we've created a new content.
+ $returnurl->param('id', $id);
+ redirect($returnurl);
+}
+
+echo $OUTPUT->header();
+$editorform->display();
+echo $OUTPUT->footer();
// Get the toolbar ready.
$toolbar = array ();
+
+// Place the Add button in the toolbar.
+if (has_capability('moodle/contentbank:useeditor', $context)) {
+ // Get the content types for which the user can use an editor.
+ $editabletypes = $cb->get_contenttypes_with_capability_feature(\core_contentbank\contenttype::CAN_EDIT, $context);
+ if (!empty($editabletypes)) {
+ // Editor base URL.
+ $editbaseurl = new moodle_url('/contentbank/edit.php', ['contextid' => $contextid]);
+ $toolbar[] = ['name' => get_string('add'), 'link' => $editbaseurl, 'dropdown' => true, 'contenttypes' => $editabletypes];
+ }
+}
+
+// Place the Upload button in the toolbar.
if (has_capability('moodle/contentbank:upload', $context)) {
// Don' show upload button if there's no plugin to support any file extension.
$accepted = $cb->get_supported_extensions_as_string($context);
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template core_contentbank/list
+ @template core_contentbank/bankcontent
Example context (json):
{
},
{
"name": "resume.pdf",
+ "title": "resume",
+ "timemodified": 1589792039,
+ "size": "699.3KB",
+ "bytes": 716126,
+ "type": "Archive (PDF)",
"icon": "http://something/theme/image.php/boost/core/1584597850/f/pdf-64"
}
],
"tools": [
+ {
+ "name": "Add",
+ "dropdown": true,
+ "link": "http://something/contentbank/edit.php?contextid=1",
+ "contenttypes": [
+ {
+ "name": "H5P Interactive Content",
+ "baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p",
+ "types": [
+ {
+ "typename": "H5P Interactive Content"
+ },
+ {
+ "typename": "Accordion",
+ "typeeditorparams": "library=Accordion-1.4",
+ "typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg"
+ }
+ ]
+ }
+ ]
+ },
{
"name": "Upload",
"link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
Example context (json):
{
"tools": [
+ {
+ "name": "Add",
+ "dropdown": true,
+ "link": "http://something/contentbank/edit.php?contextid=1",
+ "contenttypes": [
+ {
+ "name": "h5p",
+ "baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p",
+ "types": [
+ {
+ "typename": "H5P Interactive Content"
+ },
+ {
+ "typename": "Accordion",
+ "typeeditorparams": "library=Accordion-1.4",
+ "typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg"
+ }
+ ]
+ }
+ ]
+ },
{
"name": "Upload",
"link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
}}
{{#tools}}
- <a href="{{{ link }}}" class="icon-no-margin btn btn-secondary" title="{{{ name }}}">
- {{#pix}} {{{ icon }}} {{/pix}} {{{ name }}}
- </a>
+ {{#dropdown}}
+ {{>core_contentbank/bankcontent/toolbar_dropdown}}
+ {{/dropdown}}
+ {{^dropdown}}
+ <a href="{{{ link }}}" class="icon-no-margin btn btn-secondary" title="{{{ name }}}">
+ {{#pix}} {{{ icon }}} {{/pix}} {{{ name }}}
+ </a>
+ {{/dropdown}}
{{/tools}}
<button class="icon-no-margin btn btn-secondary active ml-2"
title="{{#str}} displayicons, contentbank {{/str}}"
title="{{#str}} displaydetails, contentbank {{/str}}"
data-action="viewlist">
{{#pix}}t/viewdetails, core, {{#str}} displaydetails, contentbank {{/str}} {{/pix}}
-</button>
\ No newline at end of file
+</button>
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template core_contentbank/bankcontent/toolbar_dropdown
+
+ Example context (json):
+ {
+ "name": "Add",
+ "dropdown": true,
+ "link": "http://something/contentbank/edit.php?contextid=1",
+ "contenttypes": [
+ {
+ "name": "h5p",
+ "baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p",
+ "types": [
+ {
+ "typename": "H5P Interactive Content"
+ },
+ {
+ "typename": "Accordion",
+ "typeeditorparams": "library=Accordion-1.4",
+ "typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg"
+ }
+ ]
+ }
+ ]
+ }
+
+}}
+<div class="btn-group mr-1" role="group">
+ <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" data-action="{{name}}-content"
+ aria-haspopup="true" aria-expanded="false" {{^contenttypes}}title="{{#str}}nocontenttypes, core_contentbank{{/str}}"
+ disabled{{/contenttypes}}>
+ {{#name}} {{name}} {{/name}}
+ </button>
+ <div class="dropdown-menu dropdown-scrollable dropdown-menu-right">
+ {{#contenttypes}}
+ {{#types}}
+ {{^typeeditorparams}}
+ <h6 class="dropdown-header">{{ typename }}</h6>
+ {{/typeeditorparams}}
+ {{#typeeditorparams}}
+ <a class="dropdown-item icon-size-4" href="{{{ baseurl }}}&{{{ typeeditorparams }}}">
+ <img alt="" class="icon" src="{{{ typeicon }}}"> {{ typename }}
+ </a>
+ {{/typeeditorparams}}
+ {{/types}}
+ {{/contenttypes}}
+ </div>
+</div>
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more comments.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template core_contentbank/view_content
+
+ View content page.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * contenthtml - string - content html.
+ * usercanedit - boolean - whether the user has permission to edit the content.
+ * editcontenturl - string - edit page URL.
+ * closeurl - string - close landing page.
+
+ Example context (json):
+ {
+ "contenthtml" : "<iframe src=\"http://something/h5p/embed.php?url=h5pfileurl\"></iframe>",
+ "usercanedit" : true,
+ "editcontenturl" : "http://something/contentbank/edit.php?contextid=1&plugin=h5p&id=1",
+ "closeurl" : "http://moodle.test/h5pcb/moodle/contentbank/index.php"
+ }
+}}
+<div class="core_contentbank_viewcontent">
+ <div class="d-flex justify-content-end flex-column flex-sm-row">
+ {{>core_contentbank/viewcontent/toolbarview}}
+ </div>
+ <div class="container mt-1 mb-1" data-region="viewcontent-content">
+ {{{ contenthtml }}}
+ </div>
+ <div class="d-flex justify-content-end flex-column flex-sm-row">
+ {{>core_contentbank/viewcontent/toolbarview}}
+ </div>
+</div>
--- /dev/null
+{{!
+This file is part of Moodle - http://moodle.org/
+
+Moodle is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Moodle is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more comments.
+
+You should have received a copy of the GNU General Public License
+along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template core_contentbank/viewcontent/toolbarview
+
+ Contentbank view toolbar.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * contenthtml - string - content html.
+ * usercanedit - boolean - whether the user has permission to edit the content.
+ * editcontenturl - string - edit page URL.
+ * closeurl - string - close landing page.
+
+ Example context (json):
+ {
+ "usercanedit" : true,
+ "editcontenturl" : "http://something/contentbank/edit.php?contextid=1&plugin=h5p&id=1",
+ "closeurl" : "http://moodle.test/h5pcb/moodle/contentbank/index.php"
+ }
+}}
+{{#usercanedit}}
+<div class="cb-toolbar-container mb-2">
+ <a href="{{editcontenturl}}" class="btn btn-primary" data-action="edit-content">
+ {{#str}}edit{{/str}}
+ </a>
+ <a href="{{closeurl}}" class="btn btn-secondary" data-action="close-content">
+ {{#str}}close, core_contentbank{{/str}}
+ </a>
+</div>
+{{/usercanedit}}
--- /dev/null
+@core @core_contentbank @contentbank_h5p @_file_upload @javascript
+Feature: Content bank use editor feature
+ In order to add/edit content
+ As a user
+ I need to be able to access the edition options
+
+ Background:
+ Given I log in as "admin"
+ And I am on site homepage
+ And I turn editing mode on
+ And I add the "Navigation" block if not present
+ And I configure the "Navigation" block
+ And I set the following fields to these values:
+ | Page contexts | Display throughout the entire site |
+ And I press "Save changes"
+
+ Scenario: Users see the Add button disabled if there is no content type available for creation
+ Given I click on "Site pages" "list_item" in the "Navigation" "block"
+ When I click on "Content bank" "link"
+ Then the "[data-action=Add-content]" "css_element" should be disabled
+
+ Scenario: Users can see the Add button if there is content type available for creation
+ Given I follow "Dashboard" in the user menu
+ And I follow "Manage private files..."
+ And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
+ And I click on "Save changes" "button"
+ And I click on "Site pages" "list_item" in the "Navigation" "block"
+ And I click on "Content bank" "link" in the "Navigation" "block"
+ And I click on "Upload" "link"
+ And I click on "Choose a file..." "button"
+ And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+ And I click on "filltheblanks.h5p" "link"
+ And I click on "Select this file" "button"
+ And I click on "Save changes" "button"
+ When I click on "Content bank" "link"
+ And I click on "filltheblanks.h5p" "link"
+ And I click on "Close" "link"
+ Then I click on "[data-action=Add-content]" "css_element"
+ And I should see "Fill in the Blanks"
+
+ Scenario: Users can edit content if they have the required permission
+ Given I follow "Dashboard" in the user menu
+ And I follow "Manage private files..."
+ And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
+ And I click on "Save changes" "button"
+ And I click on "Site pages" "list_item" in the "Navigation" "block"
+ And I click on "Content bank" "link" in the "Navigation" "block"
+ And I click on "Upload" "link"
+ And I click on "Choose a file..." "button"
+ And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+ And I click on "filltheblanks.h5p" "link"
+ And I click on "Select this file" "button"
+ And I click on "Save changes" "button"
+ When I click on "Content bank" "link"
+ And I click on "filltheblanks.h5p" "link"
+ Then I click on "Edit" "link"
+ And I switch to "h5p-editor-iframe" class iframe
+ And I switch to the main frame
+ And I click on "Cancel" "button"
+ And I should see "filltheblanks.h5p" in the "h1" "css_element"
+
+ Scenario: Users can create new content if they have the required permission
+ Given I navigate to "H5P > Manage H5P content types" in site administration
+ And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
+ And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
+ And I should see "H5P content types uploaded successfully"
+ And I click on "Site pages" "list_item" in the "Navigation" "block"
+ When I click on "Content bank" "link" in the "Navigation" "block"
+ And I click on "[data-action=Add-content]" "css_element"
+ Then I click on "Fill in the Blanks" "link"
+ And I switch to "h5p-editor-iframe" class iframe
+ And I switch to the main frame
+ And I click on "Cancel" "button"
+
+ Scenario: Users can't edit content if they don't have the required permission
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | user1@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 I navigate to "H5P > Manage H5P content types" in site administration
+ And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
+ And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
+ And I should see "H5P content types uploaded successfully"
+ And I log out
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I click on "Site pages" "list_item" in the "Navigation" "block"
+ And I click on "Content bank" "link"
+ And "[data-action=Add-content]" "css_element" should exist
+ When the following "permission overrides" exist:
+ | capability | permission | role | contextlevel | reference |
+ | moodle/contentbank:useeditor | Prohibit | editingteacher | System | |
+ And I reload the page
+ Then "[data-action=Add-content]" "css_element" should not exist
And I add the "Navigation" block if not present
And I expand "Site pages" node
And I click on "Content bank" "link"
- When I click on "Display contentbank with file details" "button"
+ When I click on "Display content bank with file details" "button"
And I click on "Sort by Content name ascending" "button"
And "Dragon_santjordi.h5p" "text" should appear before "historybook.h5p" "text"
And "historybook.h5p" "text" should appear before "mathsbook.h5p" "text"
// Check there's no error when trying to move content context from an empty content bank.
$this->assertTrue($cb->delete_contents($systemcontext, $coursecontext));
}
+
+ /**
+ * Data provider for get_contenttypes_with_capability_feature.
+ *
+ * @return array
+ */
+ public function get_contenttypes_with_capability_feature_provider(): array {
+ return [
+ 'no-contenttypes_enabled' => [
+ 'contenttypesenabled' => [],
+ 'contenttypescanfeature' => [],
+ ],
+ 'contenttype_enabled_noeditable' => [
+ 'contenttypesenabled' => ['testable'],
+ 'contenttypescanfeature' => [],
+ ],
+ 'contenttype_enabled_editable' => [
+ 'contenttypesenabled' => ['testable'],
+ 'contenttypescanfeature' => ['testable'],
+ ],
+ 'no-contenttype_enabled_editable' => [
+ 'contenttypesenabled' => [],
+ 'contenttypescanfeature' => ['testable'],
+ ],
+ ];
+ }
+
+ /**
+ * Tests for get_contenttypes_with_capability_feature() function.
+ *
+ * @dataProvider get_contenttypes_with_capability_feature_provider
+ * @param array $contenttypesenabled Content types enabled.
+ * @param array $contenttypescanfeature Content types the user has the permission to use the feature.
+ *
+ * @covers ::get_contenttypes_with_capability_feature
+ */
+ public function test_get_contenttypes_with_capability_feature(array $contenttypesenabled, array $contenttypescanfeature): void {
+ $this->resetAfterTest();
+
+ $cb = new contentbank();
+
+ $plugins = [];
+
+ // Content types not enabled where the user has permission to use a feature.
+ if (empty($contenttypesenabled) && !empty($contenttypescanfeature)) {
+ $enabled = false;
+
+ // Mock core_plugin_manager class and the method get_plugins_of_type.
+ $pluginmanager = $this->getMockBuilder(\core_plugin_manager::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['get_plugins_of_type'])
+ ->getMock();
+
+ // Replace protected singletoninstance reference (core_plugin_manager property) with mock object.
+ $ref = new \ReflectionProperty(\core_plugin_manager::class, 'singletoninstance');
+ $ref->setAccessible(true);
+ $ref->setValue(null, $pluginmanager);
+
+ // Return values of get_plugins_of_type method.
+ foreach ($contenttypescanfeature as $contenttypepluginname) {
+ $contenttypeplugin = new \stdClass();
+ $contenttypeplugin->name = $contenttypepluginname;
+ $contenttypeplugin->type = 'contenttype';
+ // Add the feature to the fake content type.
+ $classname = "\\contenttype_$contenttypepluginname\\contenttype";
+ $classname::$featurestotest = ['test2'];
+ $plugins[] = $contenttypeplugin;
+ }
+
+ // Set expectations and return values.
+ $pluginmanager->expects($this->once())
+ ->method('get_plugins_of_type')
+ ->with('contenttype')
+ ->willReturn($plugins);
+ } else {
+ $enabled = true;
+ // Get access to private property enabledcontenttypes.
+ $rc = new \ReflectionClass(\core_contentbank\contentbank::class);
+ $rcp = $rc->getProperty('enabledcontenttypes');
+ $rcp->setAccessible(true);
+
+ foreach ($contenttypesenabled as $contenttypename) {
+ $plugins["\\contenttype_$contenttypename\\contenttype"] = $contenttypename;
+ // Add to the testable contenttype the feature to test.
+ if (in_array($contenttypename, $contenttypescanfeature)) {
+ $classname = "\\contenttype_$contenttypename\\contenttype";
+ $classname::$featurestotest = ['test2'];
+ }
+ }
+ // Set as enabled content types only those in the test.
+ $rcp->setValue($cb, $plugins);
+ }
+
+ $actual = $cb->get_contenttypes_with_capability_feature('test2', null, $enabled);
+ $this->assertEquals($contenttypescanfeature, array_values($actual));
+ }
}
/** Feature for testing */
const CAN_TEST = 'test';
+ /** @var array Additional features for testing */
+ public static $featurestotest;
+
/**
* Returns the HTML code to render the icon for content bank contents.
*
* @return array
*/
protected function get_implemented_features(): array {
- return [self::CAN_TEST];
+ $features = [self::CAN_TEST];
+
+ if (!empty(self::$featurestotest)) {
+ $features = array_merge($features, self::$featurestotest);
+ }
+
+ return $features;
}
/**
public function get_manageable_extensions(): array {
return ['.txt', '.png', '.h5p'];
}
+
+ /**
+ * Returns the list of different types of the given content type.
+ *
+ * @return array
+ */
+ public function get_contenttype_types(): array {
+ $type = new \stdClass();
+ $type->typename = 'testable';
+
+ return [$type];
+ }
+
+ /**
+ * Returns true, so the user has permission on the feature.
+ *
+ * @return bool True if content could be edited or created. False otherwise.
+ */
+ final public function can_test2(): bool {
+ if (!$this->is_feature_supported('test2')) {
+ return false;
+ }
+
+ return true;
+ }
}
* @param context $context The context where the content will be created.
* @param bool $convert2class Whether the class should return stdClass or plugin instance.
* @param string $filepath The filepath of the file associated to the content to create.
+ * @param string $contentname The name of the content that will be created.
* @return array An array with all the records added to the content bank.
*/
public function generate_contentbank_data(?string $contenttype, int $itemstocreate = 1, int $userid = 0,
- ?\context $context = null, bool $convert2class = true, string $filepath = 'contentfile.h5p'): array {
+ ?\context $context = null, bool $convert2class = true, string $filepath = 'contentfile.h5p',
+ string $contentname = 'Test content '): array {
global $DB, $USER;
$records = [];
for ($i = 0; $i < $itemstocreate; $i++) {
// Create content.
$record = new stdClass();
- $record->name = 'Test content ' . $i;
+ // If only 1 item is being created, do not add a number suffix to the content name.
+ $record->name = ($itemstocreate === 1) ? $contentname : $contentname . $i;
$record->configdata = '';
$record->usercreated = $userid ?? $USER->id;
$PAGE->set_url(new \moodle_url('/contentbank/view.php', ['id' => $id]));
$PAGE->set_context($context);
$PAGE->navbar->add($record->name);
-$PAGE->set_heading($title);
+$PAGE->set_heading($record->name);
$title .= ": ".$record->name;
$PAGE->set_title($title);
$PAGE->set_pagetype('contenbank');
));
echo $OUTPUT->header();
-echo $OUTPUT->box_start('generalbox');
// If needed, display notifications.
if ($errormsg !== '') {
echo $OUTPUT->notification($statusmsg, 'notifysuccess');
}
if ($contenttype->can_access()) {
- echo $contenttype->get_view_content($content);
+ $viewcontent = new core_contentbank\output\viewcontent($contenttype, $content);
+ echo $OUTPUT->render($viewcontent);
+} else {
+ $message = get_string('contenttypenoaccess', 'core_contentbank', $record->contenttype);
+ echo $OUTPUT->notification($message, 'error');
}
-echo $OUTPUT->box_end();
echo $OUTPUT->footer();
}
// Delete.
- if ($category->can_delete_full()) {
+ if (!empty($category->move_content_targets_list()) || $category->can_delete_full()) {
$actions['delete'] = array(
'url' => new \moodle_url($baseurl, array('action' => 'deletecategory')),
'icon' => new \pix_icon('t/delete', new \lang_string('delete')),
$html .= html_writer::end_div();
$html .= $icon;
if ($hasactions) {
- $textattributes = array('class' => 'float-left categoryname');
+ $textattributes = array('class' => 'float-left categoryname aalink');
} else {
$textattributes = array('class' => 'float-left categoryname without-actions');
}
'for' => 'courselistitem' . $course->id));
$html .= html_writer::end_div();
$html .= html_writer::end_div();
- $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
+ $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename aalink'));
$html .= html_writer::start_div('float-right');
if ($course->idnumber) {
$html .= html_writer::tag('span', s($course->idnumber), array('class' => 'text-muted idnumber'));
$html .= html_writer::end_div();
}
$html .= html_writer::end_div();
- $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
+ $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename aalink'));
$html .= html_writer::tag('span', $categoryname, array('class' => 'float-left ml-3 text-muted'));
$html .= html_writer::start_div('float-right');
$html .= $this->search_listitem_actions($course);
'class' => 'iconlarge activityicon', 'alt' => '', 'role' => 'presentation', 'aria-hidden' => 'true')) .
html_writer::tag('span', $instancename . $altname, array('class' => 'instancename'));
if ($mod->uservisible) {
- $output .= html_writer::link($url, $activitylink, array('class' => $linkclasses, 'onclick' => $onclick));
+ $output .= html_writer::link($url, $activitylink, array('class' => 'aalink' . $linkclasses, 'onclick' => $onclick));
} else {
// We may be displaying this just in order to show information
// about visibility, without the actual link ($mod->is_visible_on_course_page()).
}
$coursename = $chelper->get_course_formatted_name($course);
$coursenamelink = html_writer::link(new moodle_url('/course/view.php', ['id' => $course->id]),
- $coursename, ['class' => $course->visible ? '' : 'dimmed']);
+ $coursename, ['class' => $course->visible ? 'aalink' : 'aalink dimmed']);
$content .= html_writer::tag($nametag, $coursenamelink, ['class' => 'coursename']);
// If we display course in collapsed form but the course has summary or course contacts, display the link to the info page.
$content .= html_writer::start_tag('div', ['class' => 'moreinfo']);
}
$content .= html_writer::start_tag('div', array('class' => 'info'));
- $content .= html_writer::tag(($depth > 1) ? 'h4' : 'h3', $categoryname, array('class' => 'categoryname'));
+ $content .= html_writer::tag(($depth > 1) ? 'h4' : 'h3', $categoryname, array('class' => 'categoryname aabtn'));
$content .= html_writer::end_tag('div'); // .info
// add category content to the output
if ($coursecat->get_children_count()) {
$classes = array(
- 'collapseexpand',
+ 'collapseexpand', 'aabtn'
);
// Check if the category content contains subcategories with children's content loaded.
}
$output = html_writer::link('#' . $skipdivid,
get_string('skipa', 'access', core_text::strtolower(strip_tags($header))),
- array('class' => 'skip-block skip'));
+ array('class' => 'skip-block skip aabtn'));
// Wrap frontpage part in div container.
$output .= html_writer::start_tag('div', array('id' => $contentsdivid));
}
}}
-<div data-region="carousel" class="carousel slide">
- <div class="carousel-inner" aria-live="polite">
- <div class="carousel-item px-4 py-3 active" data-region="modules">
- <div class="modchoosercontainer" aria-label="{{#str}} activitymodules, core {{/str}}">
+<div data-region="carousel" class="carousel slide d-flex flex-fill">
+ <div class="carousel-inner d-flex flex-fill" aria-live="polite">
+ <div class="carousel-item p-3 active" data-region="modules">
+ <div class="modchoosercontainer d-flex flex-column flex-fill" aria-label="{{#str}} activitymodules, core {{/str}}">
<div class="searchcontainer mb-3">
{{>core_course/local/activitychooser/search}}
</div>
- <div data-region="chooser-container">
- <div class="nav nav-tabs mb-3" id="activities-{{uniqid}}" role="tablist">
+ <div data-region="chooser-container" class="chooser-container">
+ <div class="nav nav-tabs 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"
{{#str}} recommended, core {{/str}}
</a>
</div>
- <div class="tab-content" id="tabbed-activities-{{uniqid}}">
+ <div class="tab-content flex-fill border-left border-right border-bottom bg-light" id="tabbed-activities-{{uniqid}}">
<div class="tab-pane {{#favouritesFirst}}active{{/favouritesFirst}}" id="starred-{{uniqid}}" data-region="favourites" role="tabpanel" aria-labelledby="starred-tab-{{uniqid}}">
<div class="optionscontainer d-flex flex-wrap p-1 mw-100 position-relative" role="menubar" data-region="chooser-options-container" data-render="favourites-area">
{{>core_course/local/activitychooser/favourites}}
</div>
{{/showshortname}}
</div>
- <a href="{{viewurl}}" class="coursename mr-2">
+ <a href="{{viewurl}}" class="aalink coursename mr-2">
{{> core_course/favouriteicon }}
<span class="sr-only">
{{#str}}aria:coursename, core_course{{/str}}
}
}}
<div class="optionsummary" tabindex="-1" data-region="chooser-option-summary-container" aria-labelledby="optionsummary_label" aria-describedby="optionsumary_desc">
- <div class="content text-left mb-5 px-5 py-4" data-region="chooser-option-summary-content-container">
+ <div class="content text-left mb-5 p-2 px-sm-5 py-sm-4" data-region="chooser-option-summary-content-container">
<div class="heading mb-4">
<h5 id="optionsummary_label-{{uniqid}}" data-region="summary-header" tabindex="0">
{{{icon}}}
"icon": "<img class='icon' src='http://urltooptionicon'>"
}
}}
-<div role="menuitem" tabindex="-1" aria-label="{{title}}" class="option d-block text-center py-1 mb-1" data-region="chooser-option-container" data-internal="{{name}}" data-modname="{{componentname}}_{{link}}">
- <div class="optioninfo w-100" data-region="chooser-option-info-container">
- <a class="d-block" href="{{link}}" title="{{#str}} addnew, moodle, {{title}} {{/str}}" tabindex="-1" data-action="add-chooser-option">
- <span class="optionicon d-block">
+<div role="menuitem" tabindex="-1" aria-label="{{title}}" class="option border-0 card m-1 bg-white" data-region="chooser-option-container" data-internal="{{name}}" data-modname="{{componentname}}_{{link}}">
+ <div class="optioninfo card-body d-flex flex-column text-center p-1" data-region="chooser-option-info-container">
+ <a class="d-flex flex-column justify-content-between flex-fill" href="{{link}}" title="{{#str}} addnew, moodle, {{title}} {{/str}}" tabindex="-1" data-action="add-chooser-option">
+ <div class="optionicon mt-2 mb-1 icon-size-5 icon-no-margin">
{{{icon}}}
- </span>
- <p class="optionname d-block mt-2 mb-0"> {{#shortentext}}20, {{title}}{{/shortentext}}</p>
+ </div>
+ <div class="optionname clamp-2">{{title}}</div>
</a>
- <div class="optionactions btn-group" role="group" data-region="chooser-option-actions-container">
+ <div class="optionactions d-flex justify-content-center" role="group" data-region="chooser-option-actions-container">
{{^legacyitem}}
<button class="btn btn-icon icon-no-margin icon-size-3 m-0 optionaction {{#favourite}}text-primary{{/favourite}}{{^favourite}}text-muted{{/favourite}}"
data-action="manage-module-favourite"
}
}}
<p class="mt-4 px-3 pb-1">{{#str}} resultsfound, core, {{searchresultsnumber}} {{/str}}</p>
-<div class="searchresultitemscontainer d-flex flex-wrap mw-100 position-relative p-1 mt-4" role="menubar" data-region="search-result-items-container">
+<div class="bg-light searchresultitemscontainer-wrapper border">
+<div class="searchresultitemscontainer d-flex flex-wrap mw-100 position-relative p-1" role="menubar" data-region="search-result-items-container">
{{#searchresults}}
{{>core_course/local/activitychooser/item}}
{{/searchresults}}
</div>
+</div>
// Clicks the selected activity if it exists.
$activityxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' modchooser ')]" .
"/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optioninfo ')]" .
- "/descendant::p[contains(concat(' ', normalize-space(@class), ' '), ' optionname ')]" .
+ "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optionname ')]" .
"[normalize-space(.)=$activityliteral]" .
"/parent::a";
$exception = new ExpectationException('"' . $categoryname . '" category can not be found', $this->getSession());
$categoryliteral = behat_context_helper::escape($categoryname);
- $xpath = "//div[@class='info']/descendant::*[" . implode(' or ', $headingtags) . "][@class='categoryname'][./descendant::a[.=$categoryliteral]]";
+ $xpath = "//div[@class='info']/descendant::*[" . implode(' or ', $headingtags) .
+ "][contains(@class,'categoryname')][./descendant::a[.=$categoryliteral]]";
$node = $this->find('xpath', $xpath, $exception);
$node->click();
$node->find('css', 'a.categoryname')->click();
}
+ /**
+ * Locates a category in the course category management interface and then opens action menu for it.
+ *
+ * @Given /^I open the action menu for "(?P<name_string>(?:[^"]|\\")*)" in management category listing$/
+ *
+ * @param string $name The name of the category as it is displayed in the management interface.
+ */
+ public function i_open_the_action_menu_for_item_in_management_category_listing($name) {
+ $node = $this->get_management_category_listing_node_by_name($name);
+ $node->find('xpath', "//*[contains(@class, 'category-item-actions')]//a[@data-toggle='dropdown']")->click();
+ }
+
+ /**
+ * Checks that the specified category actions menu contains an item.
+ *
+ * @Then /^"(?P<name_string>(?:[^"]|\\")*)" category actions menu should have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
+ *
+ * @param string $name
+ * @param string $menuitem
+ * @throws Behat\Mink\Exception\ExpectationException
+ */
+ public function category_actions_menu_should_have_item($name, $menuitem) {
+ $node = $this->get_management_category_listing_node_by_name($name);
+
+ $notfoundexception = new ExpectationException('"' . $name . '" doesn\'t have a "' .
+ $menuitem . '" item', $this->getSession());
+ $this->find('named_partial', ['link', $menuitem], $notfoundexception, $node);
+ }
+
+ /**
+ * Checks that the specified category actions menu does not contain an item.
+ *
+ * @Then /^"(?P<name_string>(?:[^"]|\\")*)" category actions menu should not have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
+ *
+ * @param string $name
+ * @param string $menuitem
+ * @throws Behat\Mink\Exception\ExpectationException
+ */
+ public function category_actions_menu_should_not_have_item($name, $menuitem) {
+ $node = $this->get_management_category_listing_node_by_name($name);
+
+ try {
+ $this->find('named_partial', ['link', $menuitem], false, $node);
+ throw new ExpectationException('"' . $name . '" has a "' . $menuitem .
+ '" item when it should not', $this->getSession());
+ } catch (ElementNotFoundException $e) {
+ // This is good, the menu item should not be there.
+ }
+ }
+
/**
* Go to the course participants
*
Test we can create a sub category
Test we can edit a category
Test we can delete a category
+ Test deleting categories interface when user permissions are restricted
Test we can move a category
Test we can assign roles within a category
Test we can set permissions on a category
And I should not see "Cat 3" in the "#category-listing ul" "css_element"
And I should see "Course 1" in the "#course-listing ul.course-list" "css_element"
+ Scenario: Test deleting categories action is not listed when permissions are restricted.
+ Given the following "users" exist:
+ | username | firstname | lastname |
+ | manager | Manager | Manager |
+ And the following "categories" exist:
+ | name | category | idnumber |
+ | Cat 1 | 0 | CAT1 |
+ | Cat 2 | 0 | CAT2 |
+ And the following "courses" exist:
+ | category | fullname | shortname |
+ | CAT1 | Course 1 | C1 |
+ And the following "system role assigns" exist:
+ | user | role | contextlevel |
+ | manager | manager | System |
+ And the following "permission overrides" exist:
+ | capability | permission | role | contextlevel | reference |
+ | moodle/course:delete | Prevent | manager | Course | C1 |
+ | moodle/course:create | Prevent | manager | System | |
+
+ When I log in as "manager"
+ And I go to the courses management page
+ Then I should see the "Course categories and courses" management page
+ And I should see "Cat 1" in the "#category-listing ul" "css_element"
+ And I should see "Cat 2" in the "#category-listing ul" "css_element"
+ And I open the action menu for "Cat 1" in management category listing
+ And "Cat 1" category actions menu should not have "Delete" item
+
+ Scenario: Test deleting categories interface when course create permission is restricted in system.
+ Given the following "users" exist:
+ | username | firstname | lastname |
+ | manager | Manager | Manager |
+ And the following "categories" exist:
+ | name | category | idnumber |
+ | Cat 1 | 0 | CAT1 |
+ | Cat 2 | 0 | CAT2 |
+ And the following "courses" exist:
+ | category | fullname | shortname |
+ | CAT1 | Course 1 | C1 |
+ And the following "system role assigns" exist:
+ | user | role | contextlevel |
+ | manager | manager | System |
+ And the following "permission overrides" exist:
+ | capability | permission | role | contextlevel | reference |
+ | moodle/course:delete | Allow | manager | Course | C1 |
+ | moodle/course:create | Prevent | manager | System | |
+
+ When I log in as "manager"
+ And I go to the courses management page
+ And I open the action menu for "Cat 1" in management category listing
+ Then "Cat 1" category actions menu should have "Delete" item
+ And I click on "delete" action for "Cat 1" in management category listing
+ # Redirect
+ And I should see "Delete category: Cat 1"
+ And I should see "Contents of Cat 1"
+ And I should see "Delete all - cannot be undone"
+ And "What to do" "select" should not exist
+ And "Move into" "select" should not exist
+ And I press "Cancel"
+
+ Scenario: Test deleting categories interface when course delete permission is restricted for category.
+ Given the following "users" exist:
+ | username | firstname | lastname |
+ | manager | Manager | Manager |
+ And the following "categories" exist:
+ | name | category | idnumber |
+ | Cat 1 | 0 | CAT1 |
+ | Cat 2 | 0 | CAT2 |
+ And the following "courses" exist:
+ | category | fullname | shortname |
+ | CAT1 | Course 1 | C1 |
+ And the following "system role assigns" exist:
+ | user | role | contextlevel |
+ | manager | manager | System |
+ And the following "permission overrides" exist:
+ | capability | permission | role | contextlevel | reference |
+ | moodle/course:delete | Prevent | manager | Course | C1 |
+ | moodle/course:create | Allow | manager | System | |
+
+ When I log in as "manager"
+ And I go to the courses management page
+ And I open the action menu for "Cat 1" in management category listing
+ Then "Cat 1" category actions menu should have "Delete" item
+ And I click on "delete" action for "Cat 1" in management category listing
+ # Redirect
+ And I should see "Delete category: Cat 1"
+ And I should see "Contents of Cat 1"
+ And I should see "Move contents to another category"
+ And "What to do" "select" should not exist
+ And "Move into" "select" should exist
+ And the "Move into" select box should contain "Cat 2"
+ And the "Move into" select box should contain "Miscellaneous"
+ And I press "Cancel"
+
+ @javascript
+ Scenario: Test deleting categories interface when course create permissions are restricted for some categories.
+ Given the following "users" exist:
+ | username | firstname | lastname |
+ | manager | Manager | Manager |
+ And the following "categories" exist:
+ | name | category | idnumber |
+ | Cat 1 | 0 | CAT1 |
+ | Cat 2 | 0 | CAT2 |
+ And the following "courses" exist:
+ | category | fullname | shortname |
+ | CAT1 | Course 1 | C1 |
+ And the following "system role assigns" exist:
+ | user | role | contextlevel |
+ | manager | manager | System |
+ And the following "permission overrides" exist:
+ | capability | permission | role | contextlevel | reference |
+ | moodle/course:delete | Allow | manager | Course | C1 |
+ | moodle/course:create | Allow | manager | System | |
+ | moodle/course:create | Prevent | manager | Category | CAT2 |
+
+ When I log in as "manager"
+ And I go to the courses management page
+ And I open the action menu for "Cat 1" in management category listing
+ Then "Cat 1" category actions menu should have "Delete" item
+ And I click on "delete" action for "Cat 1" in management category listing
+ # Redirect
+ And I should see "Delete category: Cat 1"
+ And I should see "Contents of Cat 1"
+ And "What to do" "select" should exist
+ And "Move into" "select" should exist
+ And the "Move into" select box should not contain "Cat 2"
+ And the "Move into" select box should contain "Miscellaneous"
+ And I set the field "What to do" to "Delete all - cannot be undone"
+ And "Move into" "select" should not be visible
+ And I press "Cancel"
+
Scenario: Test I can assign roles for a category through the management interface.
Given the following "categories" exist:
| name | category | idnumber |
protected function fm_js_template_iconfilename() {
$rv = '
<div class="fp-file">
- <a href="#">
+ <a href="#" class="d-block aabtn">
<div style="position:relative;">
<div class="fp-thumbnail"></div>
<div class="fp-reficons1"></div>
<div class="fp-filename text-truncate"></div>
</div>
</a>
- <a class="fp-contextmenu" href="#">'.$this->pix_icon('i/menu', 'â–¶').'</a>
+ <a class="fp-contextmenu btn btn-icon btn-light border icon-no-margin icon-size-3" href="#">
+ <span>'.$this->pix_icon('i/menu', 'â–¶').'</span></a>
</div>';
return $rv;
}
<div class="filemanager fp-mkdir-dlg" role="dialog" aria-live="assertive" aria-labelledby="fp-mkdir-dlg-title">
<div class="fp-mkdir-dlg-text">
<label id="fp-mkdir-dlg-title">' . get_string('newfoldername', 'repository') . '</label><br/>
- <input type="text" />
+ <input type="text" class="form-control"/>
</div>
<button class="fp-dlg-butcreate btn-primary btn">'.get_string('makeafolder').'</button>
<button class="fp-dlg-butcancel btn-cancel btn">'.get_string('cancel').'</button>
*/
protected function fm_js_template_fileselectlayout() {
$context = [
- 'helpicon' => $this->help_icon('setmainfile', 'repository')
+ 'helpicon' => $this->help_icon('setmainfile', 'repository'),
+ 'licensehelpicon' => $this->create_license_help_icon_context(),
+ 'columns' => true
];
return $this->render_from_template('core/filemanager_fileselect', $context);
}
* @return string
*/
protected function fp_js_template_selectlayout() {
- return $this->render_from_template('core/filemanager_selectlayout', []);
+ $context = [
+ 'licensehelpicon' => $this->create_license_help_icon_context()
+ ];
+ return $this->render_from_template('core/filemanager_selectlayout', $context);
}
/**
* @return string
*/
protected function fp_js_template_uploadform() {
- return $this->render_from_template('core/filemanager_uploadform', []);
+ $context = [
+ 'licensehelpicon' => $this->create_license_help_icon_context()
+ ];
+ return $this->render_from_template('core/filemanager_uploadform', $context);
}
/**
public function repository_default_searchform() {
return $this->render_from_template('core/filemanager_default_searchform', []);
}
+
+ /**
+ * Create the context for rendering help icon with license links displaying all licenses and sources.
+ *
+ * @return \stdClass $iconcontext the context for rendering license help info.
+ */
+ protected function create_license_help_icon_context() : stdClass {
+ $licensecontext = new stdClass();
+
+ $licenses = [];
+ // Discard licenses without a name or source from enabled licenses.
+ foreach (license_manager::get_active_licenses() as $license) {
+ if (!empty($license->fullname) && !empty($license->source)) {
+ $licenses[] = $license;
+ }
+ }
+
+ $licensecontext->licenses = $licenses;
+ $helptext = $this->render_from_template('core/filemanager_licenselinks', $licensecontext);
+
+ $iconcontext = new stdClass();
+ $iconcontext->text = $helptext;
+ $iconcontext->alt = get_string('helpprefix2', 'moodle', get_string('chooselicense', 'repository'));
+
+ return $iconcontext;
+ }
}
/**
--- /dev/null
+@core @core_files
+Feature: View license links
+ In order to get select the applicable license when uploading a file
+ As a user
+ I need to be able to navigate to a page containing license terms from the file manager
+
+ @javascript
+ Scenario: Uploading a file displays license list modal
+ Given I log in as "admin"
+ And I follow "Manage private files..."
+ And I wait until the page is ready
+ And I follow "Add..."
+ And I follow "Upload a file"
+ And I click on "Help with Choose license" "icon" in the "File picker" "dialogue"
+ Then I should see "Follow these links for further information on the available license options:"
+
+ @javascript @_file_upload
+ Scenario: Altering a file should display license list modal
+ Given I log in as "admin"
+ And I follow "Manage private files..."
+ And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
+ And I press "Save changes"
+ And I follow "Manage private files..."
+ And I click on "empty.txt" "link"
+ And I click on "Help with Choose license" "icon"
+ Then I should see "Follow these links for further information on the available license options:"
+
+ @javascript @_file_upload
+ Scenario: Recent files should display license list modal
+ Given I log in as "admin"
+ And I follow "Manage private files..."
+ And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
+ And I press "Save changes"
+ And I follow "Manage private files..."
+ And I follow "Add..."
+ And I click on "Recent files" "link" in the "File picker" "dialogue"
+ And I click on "empty.txt" "link" in the "File picker" "dialogue"
+ And I click on "Help with Choose license" "icon" in the ".fp-setlicense" "css_element"
+ Then I should see "Follow these links for further information on the available license options:"
+
+ @javascript @_file_upload
+ Scenario: Private files should display license list modal
+ Given I log in as "admin"
+ And I follow "Manage private files..."
+ And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
+ And I press "Save changes"
+ And I follow "Manage private files..."
+ And I follow "Add..."
+ And I click on "Private files" "link" in the "File picker" "dialogue"
+ And I click on "empty.txt" "link" in the "File picker" "dialogue"
+ And I click on "Help with Choose license" "icon" in the ".fp-setlicense" "css_element"
+ Then I should see "Follow these links for further information on the available license options:"
And I wait "1" seconds
Then the field "Guide criterion B criterion remark" matches value "Comment \"4\""
When I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I follow "Edit settings"
And I follow "Test assignment 1 name"
And I navigate to "View all submissions" in current page administration
public function i_save_the_advanced_grading_form() {
$this->execute('behat_forms::press_button', get_string('savechanges'));
- $this->execute('behat_forms::press_button', 'Ok');
+ $this->execute('behat_forms::press_button', 'OK');
$this->execute('behat_general::i_click_on', array($this->escape(get_string('editsettings')), 'link'));
$this->execute('behat_forms::press_button', get_string('cancel'));
$this->execute('behat_navigation::i_navigate_to_in_current_page_administration',
+++ /dev/null
-.gradeimport_data_area {
- margin: 0 0 10px;
- width: 475px;
- height: 209px;
-}
return '';
}
- return $OUTPUT->action_icon($url, new pix_icon('t/preview',
- get_string('gradeanalysis', 'core_grades')));
+ $title = get_string('gradeanalysis', 'core_grades');
+ return $OUTPUT->action_icon($url, new pix_icon('t/preview', ''), null,
+ ['title' => $title, 'aria-label' => $title]);
}
/**
$rows = $this->get_left_icons_row($rows, $colspan);
$suspendedstring = null;
+
+ $usercount = 0;
foreach ($this->users as $userid => $user) {
$userrow = new html_table_row();
$userrow->id = 'fixed_user_'.$userid;
+ $userrow->attributes['class'] = ($usercount % 2) ? 'userrow even' : 'userrow odd';
$usercell = new html_table_cell();
- $usercell->attributes['class'] = 'header user';
+ $usercell->attributes['class'] = ($usercount % 2) ? 'header user even' : 'header user odd';
+ $usercount++;
$usercell->header = true;
$usercell->scope = 'row';
white-space: nowrap;
}
-/**
- * Stripped table.
- */
-.path-grade-report-grader .gradeparent tr:nth-of-type(even) .cell {
- background-color: #f9f9f9;
-}
-
/**
* All the floating divs.
*/
text-align: left;
}
-/**
- * All the floating cells.
- */
-.path-grade-report-grader .gradeparent .floater .cell {
- background-color: #f9f9f9;
-}
-
/**
* The user cells.
*/
float: left;
}
-.path-grade-report .gradeparent .floater .controls.cell,
-.path-grade-report-grader .gradeparent .controls {
- background-color: #f3ead8;
-}
-
.path-grade-report-grader .gradeparent .category {
text-align: left;
}
$url = new moodle_url("/user/view.php", array('id' => $item->id, 'course' => $this->courseid));
$iconstring = get_string('filtergrades', 'gradereport_singleview', $fullname);
$grade->label = $fullname;
+ $userpic = $OUTPUT->user_picture($item, ['link' => false, 'visibletoscreenreaders' => false]);
$line = array(
- $OUTPUT->action_icon($this->format_link('user', $item->id), new pix_icon('t/editstring', $iconstring)),
- $OUTPUT->user_picture($item, array('visibletoscreenreaders' => false)) .
- html_writer::link($url, $fullname),
+ $OUTPUT->action_icon($this->format_link('user', $item->id), new pix_icon('t/editstring', ''), null,
+ ['title' => $iconstring, 'aria-label' => $iconstring]),
+ html_writer::link($url, $userpic . $fullname),
$this->item_range()
);
$lineclasses = array(
$summary = $this->summary();
if (!empty($summary)) {
- $table->summary = $summary;
+ $table->caption = $summary;
+ $table->captionhide = true;
}
// To be used for extra formatting.
$grade->label = $item->get_name();
$line = array(
- $OUTPUT->action_icon($this->format_link('grade', $item->id), new pix_icon('t/editstring', $iconstring)),
+ $OUTPUT->action_icon($this->format_link('grade', $item->id), new pix_icon('t/editstring', ''), null,
+ ['title' => $iconstring, 'aria-label' => $iconstring]),
$this->format_icon($item) . $lockicon . $itemlabel,
$this->category($item),
new range($item)
"disabled": "true"
}
}}
-<label for="{{name}}" class="accesshide">{{label}}</label>
+{{#label}}<label for="{{name}}" class="accesshide">{{label}}</label>{{/label}}
<input id="{{name}}" name="{{name}}" type="text" value="{{value}}" class="form-control" {{#tabindex}}tabindex="{{.}}"{{/tabindex}} {{#disabled}}disabled{{/disabled}}>
<input type="hidden" name="old{{name}}" value="{{value}}">
And I set the following fields to these values:
| Grade out of 100 | 50 |
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I am on "Course 1" course homepage
And I navigate to "View > Grader report" in the course gradebook
And I follow "Single view for Test assignment one"
And I set the following fields to these values:
| Grade out of 100 | 50 |
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I am on "Course 1" course homepage
And I navigate to "View > Grader report" in the course gradebook
# And I click on "input[title='Dock Navigation block']" "css_element"
And I click on "Grade" "link" in the "Student 1" "table_row"
And I set the field "Grade" to "A"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "[data-action=next-user]" "css_element"
And I set the field "Grade" to "B"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "[data-action=next-user]" "css_element"
And I set the field "Grade" to "C"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "[data-action=next-user]" "css_element"
And I set the field "Grade" to "D"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "[data-action=next-user]" "css_element"
And I set the field "Grade" to "F"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I am on "Course 1" course homepage
And I navigate to "Setup > Course grade settings" in the course gradebook
And I set the field "Show weightings" to "Show"
And I click on "Grade" "link" in the "Student 1" "table_row"
And I set the field "Grade" to "A"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I am on "Course 1" course homepage
And I navigate to "Setup > Course grade settings" in the course gradebook
And I set the field "Show weightings" to "Show"
$string['editor:contenttypebackbuttonlabel'] = 'Back';
$string['editor:contenttypecacheoutdated'] = 'Content type list outdated';
$string['editor:contenttypecacheoutdateddesc'] = 'Your site is having difficulties connecting to H5P.org to check for content type updates. You may not be able to update or install new content types.';
-$string['editor:contenttypedemobuttonlabel'] = 'Content Demo';
+$string['editor:contenttypedemobuttonlabel'] = 'Content demo';
$string['editor:contenttypedetailbuttonlabel'] = 'Details';
$string['editor:contenttypegetbuttonlabel'] = 'Get';
$string['editor:contenttypeiconalttext'] = 'Icon';
$string['editor:contenttypeowner'] = 'By :owner';
$string['editor:contenttyperestricted'] = 'Restricted content type';
$string['editor:contenttyperestricteddesc'] = 'The use of this content type has been restricted by an administrator.';
-$string['editor:contenttypesearchfieldplaceholder'] = 'Search for Content Types';
-$string['editor:contenttypesectionall'] = 'All Content Types';
+$string['editor:contenttypesearchfieldplaceholder'] = 'Search for content types';
+$string['editor:contenttypesectionall'] = 'All content types';
$string['editor:contenttypeunsupportedapiversioncontent'] = 'Contact your system administrator to provide you with the necessary updates';
$string['editor:contenttypeunsupportedapiversiontitle'] = 'This content type requires a newer core version';
$string['editor:contenttypeupdateavailable'] = 'Update available';
$string['editor:contenttypeupdatesuccess'] = ':contentType successfully updated!';
$string['editor:contenttypeupdatingbuttonlabel'] = 'Updating';
$string['editor:contenttypeusebuttonlabel'] = 'Use';
-$string['editor:contributetranslations'] = 'If you want to complete the translation for :language you can learn about <a href=":url" target="_new">contributing translations to H5P</a>';
+$string['editor:contributetranslations'] = 'If you want to complete the translation for :language, see <a href=":url" target="_new">contributing translations to H5P</a>.';
$string['editor:copiedbutton'] = 'Copied';
$string['editor:copiedtoclipboard'] = 'Content is copied to the clipboard';
$string['editor:copybutton'] = 'Copy';
$string['editor:copytoclipboard'] = 'Copy H5P content to the clipboard';
-$string['editor:createcontenttablabel'] = 'Create Content';
+$string['editor:createcontenttablabel'] = 'Create content';
$string['editor:currentmenuselected'] = 'current selection';
$string['editor:editcopyright'] = 'Edit copyright';
$string['editor:editimage'] = 'Edit image';
$string['editor:filetolarge'] = 'The file you are trying to upload might be too large.';
$string['editor:fillinthefieldsbelow'] = 'Fill in the fields below';
$string['editor:gethelp'] = 'Get help';
-$string['editor:h5pfileuploadservererrorcontent'] = 'An unexpected error occured. Check your server error log for\' + \' more details.';
+$string['editor:h5pfileuploadservererrorcontent'] = 'An unexpected error occurred. Check your server error log for more details.';
$string['editor:h5pfileuploadservererrortitle'] = 'The H5P file could not be uploaded';
-$string['editor:h5pfilevalidationfailedcontent'] = 'Make sure the uploaded H5P contains valid H5P content. H5P\' + \' files containing only libraries should be uploaded through the H5P Libraries page.';
+$string['editor:h5pfilevalidationfailedcontent'] = 'Make sure the uploaded H5P contains valid H5P content. H5P files containing only libraries should be uploaded through the H5P libraries page.';
$string['editor:h5pfilevalidationfailedtitle'] = 'Could not validate H5P file.';
$string['editor:h5pfilewrongextensioncontent'] = 'Only files with the .h5p extension are allowed.';
$string['editor:h5pfilewrongextensiontitle'] = 'The selected file could not be uploaded';
$string['editor:licensecandistribute'] = 'Can distribute';
$string['editor:licensecanholdliable'] = 'Can hold liable';
$string['editor:licensecanmodify'] = 'Can modify';
-$string['editor:licensecansublicense'] = 'Can sublicense';
+$string['editor:licensecansublicense'] = 'Can sub-licence';
$string['editor:licensecanusecommercially'] = 'Can use commercially';
$string['editor:licensecannotholdliable'] = 'Cannot hold liable';
-$string['editor:licensedescription'] = 'Some of the features of this license are indicated below. Click the info icon above to read the original license text.';
-$string['editor:licensefetchdetailsfailed'] = 'Failed fetching license details';
-$string['editor:licensemodalsubtitle'] = 'Select a license to view information about proper usage';
-$string['editor:licensemodaltitle'] = 'License Details';
+$string['editor:licensedescription'] = 'Some of the features of this licence are indicated below. Click the info icon above to read the original licence text.';
+$string['editor:licensefetchdetailsfailed'] = 'Failed fetching licence details';
+$string['editor:licensemodalsubtitle'] = 'Select a licence to view information about proper usage';
+$string['editor:licensemodaltitle'] = 'Licence details';
$string['editor:licensemustincludecopyright'] = 'Must include copyright';
-$string['editor:licensemustincludelicense'] = 'Must include license';
+$string['editor:licensemustincludelicense'] = 'Must include licence';
$string['editor:licenseunspecified'] = 'Unspecified';
$string['editor:listbelowmin'] = 'The list needs at least :min items for the content to function properly.';
$string['editor:listexceedsmax'] = 'The list exceeds the maximum of :max items.';
$string['editor:loggedchanges'] = 'Logged changes';
$string['editor:maxscoresemanticsmissing'] = 'Could not find the expected semantics in the content.';
$string['editor:metadata'] = 'Metadata';
-$string['editor:metadatasharingandlicensinginfo'] = 'Metadata (sharing and licensing info)';
+$string['editor:metadatasharingandlicensinginfo'] = 'Metadata (sharing and licencing info)';
$string['editor:missingproperty'] = 'Field :index is missing its :property property.';
$string['editor:missingtranslation'] = '[Missing translation :key]';
$string['editor:newchangehasbeenlogged'] = 'New change has been logged';
-$string['editor:newestfirst'] = 'Newest First';
+$string['editor:newestfirst'] = 'Newest first';
$string['editor:nextimage'] = 'Next image';
$string['editor:nochangeshavebeenlogged'] = 'No changes have been logged';
$string['editor:nocontenttypesavailable'] = 'No content types are available';
$string['editor:noresultsfound'] = 'No results found';
$string['editor:noresultsfounddesc'] = 'There is no content type that matches your search criteria.';
$string['editor:nosemantics'] = 'Error, could not load the content type form.';
-$string['editor:notalltextschanged'] = 'Not all texts were changed, there is only partial coverage for :language.';
+$string['editor:notalltextschanged'] = 'Not all texts were changed, as there is only partial coverage for :language.';
$string['editor:notimagefield'] = '":path" is not an image.';
$string['editor:notimageordimensionsfield'] = '":path" is not an image or dimensions field.';
$string['editor:numresults'] = ':num results';
$string['editor:orderitemdown'] = 'Order item down';
$string['editor:orderitemup'] = 'Order item up';
$string['editor:outofstep'] = 'The :property value can only be changed in steps of :step.';
-$string['editor:pasteandreplacebutton'] = 'Paste & Replace';
-$string['editor:pasteandreplacefromclipboard'] = 'Replace existing content with H5P Content from the clipboard';
+$string['editor:pasteandreplacebutton'] = 'Paste and replace';
+$string['editor:pasteandreplacefromclipboard'] = 'Replace existing content with H5P content from the clipboard';
$string['editor:pastebutton'] = 'Paste';
-$string['editor:pastecontent'] = 'Replace Content';
+$string['editor:pastecontent'] = 'Replace content';
$string['editor:pastecontentnotsupported'] = 'The content in the H5P clipboard is not supported in this context';
$string['editor:pastecontentrestricted'] = 'The content in the clipboard has been restricted on this site';
$string['editor:pasteerror'] = 'Cannot paste from clipboard';
$string['editor:pastefromclipboard'] = 'Paste H5P content from the clipboard';
$string['editor:pastenocontent'] = 'No H5P content on the clipboard';
-$string['editor:pastetoonew'] = 'The content in the H5P clipboard is of a higher version (:clip) than what is supported in this context (:local), if possible try to have this content upgraded first, and then try pasting the content here again.';
-$string['editor:pastetooold'] = 'The content in the H5P clipboard is of a lower version (:clip) than what is supported in this context (:local), if possible try to have the content you want to paste upgraded, copy it again and try pasting it here.';
-$string['editor:popularfirst'] = 'Popular First';
+$string['editor:pastetoonew'] = 'The content in the H5P clipboard is of a higher version (:clip) than what is supported in this context (:local). If possible, try to have this content upgraded first, then try pasting the content here again.';
+$string['editor:pastetooold'] = 'The content in the H5P clipboard is of a lower version (:clip) than what is supported in this context (:local). If possible, try to have the content you want to paste upgraded, then copy it again and try pasting it here.';
+$string['editor:popularfirst'] = 'Popular first';
$string['editor:previousimage'] = 'Previous image';
$string['editor:proceedbuttonlabel'] = 'Proceed to save';
$string['editor:readless'] = 'Read less';
$string['editor:readmore'] = 'Read more';
-$string['editor:recentlyusedfirst'] = 'Recently Used First';
+$string['editor:recentlyusedfirst'] = 'Recently used first';
$string['editor:reloadbuttonlabel'] = 'Reload';
$string['editor:removefile'] = 'Remove file';
$string['editor:removeimage'] = 'Remove image';
$string['editor:savemetadata'] = 'Save metadata';
$string['editor:screenshots'] = 'Screenshots';
$string['editor:scriptmissing'] = 'Could not load upgrades script for %lib.';
-$string['editor:searchresults'] = 'Search Results';
+$string['editor:searchresults'] = 'Search results';
$string['editor:selectfiletoupload'] = 'Select file to upload';
$string['editor:selectlibrary'] = 'Select the library you wish to use for your content.';
$string['editor:semanticserror'] = 'Semantics error: :error';
$string['editor:show'] = 'Show';
$string['editor:showimportantinstructions'] = 'Show instructions';
-$string['editor:tabtitlebasicfileupload'] = 'File Upload';
+$string['editor:tabtitlebasicfileupload'] = 'File upload';
$string['editor:tabtitleinputlinkurl'] = 'Link/URL';
$string['editor:textfield'] = 'text field';
$string['editor:thecontenttype'] = 'the content type';
$string['editor:thiswillpotentially'] = 'This will potentially reset all the text and translations. You can\'t undo this. The content itself will not be changed. Do you want to proceed?';
$string['editor:title'] = 'Title';
-$string['editor:toolong'] = 'Field value is too long, should contain :max letters or less.';
+$string['editor:toolong'] = 'Field value is too long; it should contain :max letters or less.';
$string['editor:tryagain'] = 'Try again';
$string['editor:tutorial'] = 'Tutorial';
$string['editor:unabletointerpreterror'] = 'Unable to interpret response.';
$string['editor:unknownlibrary'] = 'Unfortunately, the selected content type \'%lib\' isn\'t installed on this system.';
$string['editor:untitled'] = 'Untitled :libraryTitle';
$string['editor:uploadaudiotitle'] = 'Upload audio file';
-$string['editor:uploaderror'] = 'File Upload Error';
+$string['editor:uploaderror'] = 'File upload error';
$string['editor:uploadfilebuttonchangelabel'] = 'Change file';
$string['editor:uploadfilebuttonlabel'] = 'Upload a file';
$string['editor:uploadinstructionscontent'] = 'You may start with examples from <a href="https://h5p.org/content-types-and-applications" target="blank">H5P.org</a>.';
$string['configerrorlevel'] = 'Choose the amount of PHP warnings that you want to be displayed. Normal is usually the best choice.';
$string['configexportlookahead'] = 'Days to look ahead during export';
$string['configexportlookback'] = 'Days to look back during export';
-$string['configextendedusernamechars'] = 'Enable this setting to allow students to use any characters in their usernames (note this does not affect their actual names). The default is "false" which restricts usernames to be alphanumeric lowercase characters, underscore (_), hyphen (-), period (.) or at symbol (@).';
+$string['configextendedusernamechars'] = 'If enabled, usernames may include any characters except uppercase letters. Otherwise, only alphanumeric characters with lowercase letters, underscore (_), hyphen (-), period (.) and at symbol (@) are allowed.';
$string['configextramemorylimit'] = 'Some scripts like search, backup/restore or cron require more memory. Set higher values for large sites.';
$string['configfilterall'] = 'Filter all strings, including headings, titles, navigation bar and so on. This is mostly useful when using the multilang filter, otherwise it will just create extra load on your site for little gain.';
$string['configfiltermatchoneperpage'] = 'Automatic linking filters will only generate a single link for the first matching text instance found on the complete page. All others are ignored.';
$string['emailupdate'] = 'Email address update';
$string['emailupdatemessage'] = 'Dear {$a->fullname},
-You have requested a change of your email address for your user account at {$a->site}. Please open the following URL in your browser in order to confirm this change.
+You have requested a change of your email address for your account on {$a->site}. To confirm this change, please go to the following web address:
-If you have any questions please contact support on: {$a->supportemail}
+{$a->url}
-{$a->url}';
+{$a->supportemail}';
$string['emailupdatesuccess'] = 'Email address of user <em>{$a->fullname}</em> was successfully updated to <em>{$a->email}</em>.';
$string['emailupdatetitle'] = 'Confirmation of email update at {$a->site}';
$string['errormaxconsecutiveidentchars'] = 'Passwords must have at most {$a} consecutive identical characters.';
$string['confirmnewcoursecontinue'] = 'New course warning';
$string['confirmnewcoursecontinuequestion'] = 'A temporary (hidden) course will be created by the course restoration process. To abort restoration click cancel. Do not close the browser while restoring.';
$string['copiesinprogress'] = 'This course has copies in progress. <a href="{$a}">View in progress copies.</a>';
-$string['copycoursedesc'] = 'This course will be duplicated and put into the given course category.';
+$string['copycoursedesc'] = 'This course will be duplicated and put into the selected course category.';
$string['copycourseheading'] = 'Copy a course';
$string['copycoursetitle'] = 'Copy course: {$a}';
$string['copydest'] = 'Destination';
$string['copyingcourse'] = 'Course copying in progress';
$string['copyingcourseshortname'] = 'copying';
-$string['copyformfail'] = 'Ajax submission of course copy form has failed.';
+$string['copyformfail'] = 'AJAX submission of course copy form has failed.';
$string['copyop'] = 'Current operation';
$string['copyprogressheading'] = 'Course copies in progress';
$string['copyprogressheading_help'] = 'This table shows the status of all of your unfinished course copies.';
$string['backpackbadgessummary'] = 'You have {$a->totalbadges} badge(s) displayed from {$a->totalcollections} collection(s).';
$string['backpackbadgessettings'] = 'Change backpack settings';
$string['backpackcannotsendverification'] = 'Cannot send verification email';
-$string['backpackconnected'] = 'Backpack has been connected';
+$string['backpackconnected'] = 'Backpack is connected';
$string['backpackconnection'] = 'Backpack connection';
$string['backpackconnection_help'] = 'Connecting to a backpack enables you to share your badges from this site, and display public badge collections from your backpack on your profile page on this site.';
$string['backpackconnectioncancelattempt'] = 'Connect using a different email address';
$string['backpackconnectionunexpectedresult'] = 'There was a problem connecting to your backpack. Please check the credentials and try again.';
$string['backpackconnectionunexpectedmessage'] = 'The backpack returned the error: "{$a}".';
$string['backpackdetails'] = 'Backpack settings';
-$string['backpackdisconnected'] = 'Backpack has been disconnected';
+$string['backpackdisconnected'] = 'Backpack is disconnected';
$string['backpackemail'] = 'Email address';
$string['backpackemail_help'] = 'The email address associated with your backpack. While you are connected, any badges earned on this site will be associated with this email address.';
$string['backpackemailverificationpending'] = 'Verification pending';
$string['privacy:metadata:external:backpacks:image'] = 'The image of the badge';
$string['privacy:metadata:external:backpacks:issuer'] = 'Some information about the issuer';
$string['privacy:metadata:external:backpacks:url'] = 'The Moodle URL where the issued badge information can be seen';
-$string['privacy:metadata:backpackoauth2'] = 'Information oauthorization when user connect to an external backpack';
+$string['privacy:metadata:backpackoauth2'] = 'OAuth 2 information when user connects to an external backpack';
$string['privacy:metadata:backpackoauth2:userid'] = 'The ID of the user connect to backpack';
$string['privacy:metadata:backpackoauth2:usermodified'] = 'The ID of the user modified connect';
-$string['privacy:metadata:backpackoauth2:token'] = 'The token of backpack connect';
-$string['privacy:metadata:backpackoauth2:issuerid'] = 'The ID of Oauth2 service';
+$string['privacy:metadata:backpackoauth2:token'] = 'Backpack connection token';
+$string['privacy:metadata:backpackoauth2:issuerid'] = 'OAuth 2 service ID';
$string['privacy:metadata:backpackoauth2:scope'] = 'List scope of backpack connect';
$string['privacy:metadata:issued'] = 'A record of badges awarded';
$string['privacy:metadata:issued:dateexpire'] = 'The date when the badge expires';
$string['author'] = 'Author';
$string['contentbank'] = 'Content bank';
+$string['close'] = 'Close';
$string['contentdeleted'] = 'The content has been deleted.';
$string['contentname'] = 'Content name';
$string['contentnotdeleted'] = 'An error was encountered while trying to delete the content.';
$string['contentnotrenamed'] = 'An error was encountered while trying to rename the content.';
$string['contentrenamed'] = 'The content has been renamed.';
$string['contentsmoved'] = 'Content bank contents moved to {$a}.';
+$string['contenttypenoaccess'] = 'You can not view this {$a} instance';
+$string['contenttypenoedit'] = 'You can not edit contents of the {$a} content type';
$string['eventcontentcreated'] = 'Content created';
$string['eventcontentdeleted'] = 'Content deleted';
$string['eventcontentupdated'] = 'Content updated';
$string['errordeletingcontentfromcategory'] = 'Error deleting content from category {$a}.';
$string['deletecontent'] = 'Delete content';
$string['deletecontentconfirm'] = 'Are you sure you want to delete the content <em>\'{$a->name}\'</em> and all associated files? This action cannot be undone.';
-$string['displaydetails'] = 'Display contentbank with file details';
-$string['displayicons'] = 'Display contentbank with icons';
+$string['displaydetails'] = 'Display content bank with file details';
+$string['displayicons'] = 'Display content bank with icons';
$string['file'] = 'Upload content';
$string['file_help'] = 'Files may be stored in the content bank for use in courses. Only files used by content types enabled on the site may be uploaded.';
$string['itemsfound'] = '{$a} items found';
$string['lastmodified'] = 'Last modified';
$string['name'] = 'Content';
+$string['nocontenttypes'] = 'No content types available';
$string['nopermissiontodelete'] = 'You do not have permission to delete content.';
$string['nopermissiontomanage'] = 'You do not have permission to manage content.';
$string['privacy:metadata:content:contenttype'] = 'The contenttype plugin of the content in the content bank.';
$string['numyear'] = '{$a} year';
$string['numyears'] = '{$a} years';
$string['ok'] = 'OK';
-$string['okay'] = 'Ok';
$string['oldpassword'] = 'Current password';
$string['olduserdirectory'] = 'This is the OLD users directory, and is no longer needed. You may safely delete it. The files it contains have been copied to the NEW user directory.';
$string['optional'] = 'optional';
$string['editingquestion'] = 'Editing a question';
$string['editquestion'] = 'Edit question';
$string['editthiscategory'] = 'Edit this category';
-$string['emptyxml'] = 'Unkown error - empty imsmanifest.xml';
+$string['emptyxml'] = 'Unknown error - empty imsmanifest.xml';
$string['enabled'] = 'Enabled';
$string['erroraccessingcontext'] = 'Cannot access context';
$string['errordeletingquestionsfromcategory'] = 'Error deleting questions from category {$a}.';
$string['download'] = 'Download';
$string['downloadallfiles'] = 'Download all files';
$string['downloadfolder'] = 'Download all';
-$string['downloadselected'] = 'Download selected files';
-$string['deleteselected'] = 'Delete selected';
$string['downloadsucc'] = 'The file has been downloaded successfully';
$string['draftareanofiles'] = 'Cannot be downloaded because there is no files attached';
$string['editrepositoryinstance'] = 'Edit repository instance';
$string['help'] = 'Help';
$string['choosealink'] = 'Choose a link...';
$string['chooselicense'] = 'Choose license';
+$string['chooselicense_help'] = 'Follow these links for further information on the available license options:';
$string['createfolder'] = 'Create folder';
$string['iconview'] = 'View as icons';
$string['imagesize'] = '{$a->width} x {$a->height} px';
$string['nofilesattached'] = 'No files attached';
$string['nofilesavailable'] = 'No files available';
$string['nofilesselected'] = 'No files selected';
+$string['nolicenses'] = 'There are no licences available';
$string['nomorefiles'] = 'No more attachments allowed';
$string['nopathselected'] = 'No destination path select yet (double click tree node to select)';
$string['nopermissiontoaccess'] = 'No permission to access this repository.';
$string['contentbank:access'] = 'Access the content bank';
$string['contentbank:deleteanycontent'] = 'Delete any content from the content bank';
$string['contentbank:deleteowncontent'] = 'Delete content from own content bank';
-$string['contentbank:manageanycontent'] = 'Manage any content from the content bank (rename, move, publish, share, etc.)';
-$string['contentbank:manageowncontent'] = 'Manage content from own content bank (rename, move, publish, share, etc.)';
-$string['contentbank:upload'] = 'Upload new content in the content bank';
+$string['contentbank:manageanycontent'] = 'Manage any content from the content bank';
+$string['contentbank:manageowncontent'] = 'Manage content from own content bank';
+$string['contentbank:upload'] = 'Upload new content to the content bank';
+$string['contentbank:useeditor'] = 'Create or edit content using a content type editor';
$string['context'] = 'Context';
$string['course:activityvisibility'] = 'Hide/show activities';
$string['course:bulkmessaging'] = 'Send a message to many people';
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+$string['addcondition'] = 'Add condition';
+$string['adverbfor_and'] = 'and';
+$string['adverbfor_andnot'] = 'and';
+$string['adverbfor_or'] = 'or';
+$string['applyfilters'] = 'Apply filters';
+$string['clearfilterrow'] = 'Remove filter row';
+$string['clearfilters'] = 'Clear filters';
$string['countparticipantsfound'] = '{$a} participants found';
+$string['filtersetmatchdescription'] = 'How multiple filters should be combined';
+$string['match'] = 'Match';
+$string['matchofthefollowing'] = 'of the following:';
+$string['placeholdertypeorselect'] = 'Type or select...';
+$string['placeholdertype'] = 'Type...';
$string['privacy:courserequestpath'] = 'Requested courses';
$string['privacy:descriptionpath'] = 'Profile description';
$string['privacy:devicespath'] = 'User devices';
$string['privacy:profileimagespath'] = 'Profile images';
$string['privacy:privatefilespath'] = 'Private files';
$string['privacy:sessionpath'] = 'Session data';
+$string['filterbykeyword'] = 'Keyword';
+$string['selectfiltertype'] = 'Select';
$string['target:upcomingactivitiesdue'] = 'Upcoming activities due';
$string['target:upcomingactivitiesdue_help'] = 'This target generates reminders for upcoming activities due.';
$string['target:upcomingactivitiesdueinfo'] = 'All upcoming activities due insights are listed here. These students have received these insights directly.';
});
var context = $.extend({items: items}, options, state);
// Render the template.
- return templates.render('core/form_autocomplete_selection_items', context)
+ return templates.render(options.templates.items, context)
.then(function(html, js) {
// Add it to the page.
templates.replaceNodeContents(newSelection, html, js);
* @param {Boolean} showSuggestions - If suggestions should be shown
* @param {String} noSelectionString - Text to display when there is no selection
* @param {Boolean} closeSuggestionsOnSelect - Whether to close the suggestions immediately after making a selection.
+ * @param {Object} templateOverrides A set of templates to use instead of the standard templates
* @return {Promise}
*/
enhance: function(selector, tags, ajax, placeholder, caseSensitive, showSuggestions, noSelectionString,
- closeSuggestionsOnSelect) {
+ closeSuggestionsOnSelect, templateOverrides) {
// Set some default values.
var options = {
selector: selector,
placeholder: placeholder,
caseSensitive: false,
showSuggestions: true,
- noSelectionString: noSelectionString
+ noSelectionString: noSelectionString,
+ templates: $.extend({
+ input: 'core/form_autocomplete_input',
+ items: 'core/form_autocomplete_selection_items',
+ layout: 'core/form_autocomplete_layout',
+ selection: 'core/form_autocomplete_selection',
+ suggestions: 'core/form_autocomplete_suggestions',
+ }, templateOverrides),
};
var pendingKey = 'autocomplete-setup-' + selector;
M.util.js_pending(pendingKey);
// Collect rendered inline JS to be executed once the HTML is shown.
var collectedjs = '';
- var renderInput = templates.render('core/form_autocomplete_input', context).then(function(html, js) {
+ var renderLayout = templates.render(options.templates.layout, {})
+ .then(function(html) {
+ return $(html);
+ });
+
+ var renderInput = templates.render(options.templates.input, context).then(function(html, js) {
collectedjs += js;
- return html;
+ return $(html);
});
- var renderDatalist = templates.render('core/form_autocomplete_suggestions', context).then(function(html, js) {
+ var renderDatalist = templates.render(options.templates.suggestions, context).then(function(html, js) {
collectedjs += js;
- return html;
+ return $(html);
});
- var renderSelection = templates.render('core/form_autocomplete_selection', context).then(function(html, js) {
+ var renderSelection = templates.render(options.templates.selection, context).then(function(html, js) {
collectedjs += js;
- return html;
+ return $(html);
});
- return $.when(renderInput, renderDatalist, renderSelection)
- .then(function(input, suggestions, selection) {
+ return $.when(renderLayout, renderInput, renderDatalist, renderSelection)
+ .then(function(layout, input, suggestions, selection) {
originalSelect.hide();
- originalSelect.after(suggestions);
- originalSelect.after(input);
- originalSelect.after(selection);
+ var container = originalSelect.parent();
+
+ container.append(layout);
+ container.find('[data-region="form_autocomplete-input"]').replaceWith(input);
+ container.find('[data-region="form_autocomplete-suggestions"]').replaceWith(suggestions);
+ container.find('[data-region="form_autocomplete-selection"]').replaceWith(selection);
templates.runTemplateJS(collectedjs);
*
* @param {Number} contextId
* @param {Array} notificationList
+ * @param {Boolean} userLoggedIn
*/
-export const init = (contextId, notificationList) => {
+export const init = (contextId, notificationList, userLoggedIn) => {
currentContextId = contextId;
// Setup the message target region if it isn't setup already
// Add provided notifications.
addNotifications(notificationList);
- // Perform an initial poll for any new notifications.
- fetchNotifications();
+ // If the user is not logged in then we can not fetch anything for them.
+ if (userLoggedIn) {
+ // Perform an initial poll for any new notifications.
+ fetchNotifications();
+ }
};
// To maintain backwards compatability we export default here.
e.preventDefault(); // This will prevent default error dialogue.
str.get_strings([
+ {key: 'confirm', component: 'core'},
{key: 'nameuseddocombine', component: 'tag'},
- {key: 'yes'},
- {key: 'cancel'},
+ {key: 'yes', component: 'core'},
+ {key: 'cancel', component: 'core'},
])
.then(function(s) {
- return notification.confirm(e.message, s[0], s[1], s[2], function() {
+ return notification.confirm(s[0], s[1], s[2], s[3], function() {
window.location.href = window.location.href + "&newname=" + encodeURIComponent(newvalue) +
"&tagid=" + encodeURIComponent(tagid) +
'&action=renamecombine&sesskey=' + M.cfg.sesskey;
* @param {String} newHTML - HTML to insert / replace.
* @param {String} newJS - Javascript to run after the insertion.
* @param {Boolean} replaceChildNodes - Replace only the childnodes, alternative is to replace the entire node.
+ * @return {Array} The list of new DOM Nodes
*/
var domReplace = function(element, newHTML, newJS, replaceChildNodes) {
var replaceNode = $(element);
runTemplateJS(newJS);
// Notify all filters about the new content.
event.notifyFilterContentUpdated(newNodes);
+
+ return newNodes.get();
}
+
+ return [];
};
/**
* @param {jQuery|String} element - Element or selector to prepend HTML to
* @param {String} html - HTML to prepend
* @param {String} js - Javascript to run after we prepend the html
+ * @return {Array} The list of new DOM Nodes
*/
var domPrepend = function(element, html, js) {
var node = $(element);
if (node.length) {
// Prepend the html.
- node.prepend(html);
+ var newContent = $(html);
+ node.prepend(newContent);
// Run any javascript associated with the new HTML.
runTemplateJS(js);
// Notify all filters about the new content.
event.notifyFilterContentUpdated(node);
+
+ return newContent.get();
}
+
+ return [];
};
/**
* @param {jQuery|String} element - Element or selector to append HTML to
* @param {String} html - HTML to append
* @param {String} js - Javascript to run after we append the html
+ * @return {Array} The list of new DOM Nodes
*/
var domAppend = function(element, html, js) {
var node = $(element);
if (node.length) {
// Append the html.
- node.append(html);
+ var newContent = $(html);
+ node.append(newContent);
// Run any javascript associated with the new HTML.
runTemplateJS(js);
// Notify all filters about the new content.
event.notifyFilterContentUpdated(node);
+
+ return newContent.get();
}
+
+ return [];
};
return /** @alias module:core/templates */ {
* @param {JQuery} element - Element or selector to replace.
* @param {String} newHTML - HTML to insert / replace.
* @param {String} newJS - Javascript to run after the insertion.
+ * @return {Array} The list of new DOM Nodes
*/
replaceNodeContents: function(element, newHTML, newJS) {
- domReplace(element, newHTML, newJS, true);
+ return domReplace(element, newHTML, newJS, true);
},
/**
* @param {JQuery} element - Element or selector to replace.
* @param {String} newHTML - HTML to insert / replace.
* @param {String} newJS - Javascript to run after the insertion.
+ * @return {Array} The list of new DOM Nodes
*/
replaceNode: function(element, newHTML, newJS) {
- domReplace(element, newHTML, newJS, false);
+ return domReplace(element, newHTML, newJS, false);
},
/**
* @param {jQuery|String} element - Element or selector to prepend HTML to
* @param {String} html - HTML to prepend
* @param {String} js - Javascript to run after we prepend the html
+ * @return {Array} The list of new DOM Nodes
*/
prependNodeContents: function(element, html, js) {
- domPrepend(element, html, js);
+ return domPrepend(element, html, js);
},
/**
* @param {jQuery|String} element - Element or selector to append HTML to
* @param {String} html - HTML to append
* @param {String} js - Javascript to run after we append the html
+ * @return {Array} The list of new DOM Nodes
*/
appendNodeContents: function(element, html, js) {
- domAppend(element, html, js);
+ return domAppend(element, html, js);
},
};
});
$content = $contenttype->create_content($record);
if (!empty($data['filepath'])) {
+ $filename = basename($data['filepath']);
$fs = get_file_storage();
$filerecord = array(
'component' => 'contentbank',
'contextid' => $context->id,
'userid' => $data['userid'],
'itemid' => $content->get_id(),
- 'filename' => $data['contentname'],
+ 'filename' => $filename,
'filepath' => '/'
);
$fs->create_file_from_pathname($filerecord, $CFG->dirroot . $data['filepath']);
.//div[
contains(concat(' ', normalize-space(@class), ' '), ' modal ')
and
- normalize-space(descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' modal-header ')] = %locator%)
+ normalize-space(descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' modal-header ')]) = %locator%
]
XPATH
, 'group_message' => <<<XPATH
'moodle/course:viewhiddenactivities' => array(
- 'captype' => 'write',
+ 'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => array(
'teacher' => CAP_ALLOW,
'moodle/course:viewhiddensections' => array(
- 'captype' => 'write',
+ 'captype' => 'read',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => array(
'editingteacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
)
],
+
+ // Allow users to create/edit content within the content bank.
+ 'moodle/contentbank:useeditor' => [
+ 'riskbitmask' => RISK_SPAM,
+ 'captype' => 'write',
+ 'contextlevel' => CONTEXT_COURSE,
+ 'archetypes' => array(
+ 'manager' => CAP_ALLOW,
+ 'coursecreator' => CAP_ALLOW,
+ 'editingteacher' => CAP_ALLOW,
+ )
+ ],
);
*/
_getMediumProperties: function(medium) {
var boolAttr = function(elem, attr) {
- return elem.getAttribute(attr) ? true : false;
+ // As explained in MDL-64175, some OS (like Ubuntu), are removing the value for these attributes.
+ // So in order to check if attr="true", we need to check if the attribute exists and if the value is empty or true.
+ return (elem.hasAttribute(attr) && (elem.getAttribute(attr) || elem.getAttribute(attr) === ''));
};
var tracks = {
if ($calendartype->get_name() === 'gregorian') {
$image = $OUTPUT->pix_icon('i/calendar', get_string('calendar', 'calendar'), 'moodle');
$this->_elements[] = $this->createFormElement('link', 'calendar',
- null, '#', $image,
- array('class' => 'visibleifjs'));
+ null, '#', $image);
}
// If optional we add a checkbox which the user can use to turn if on
if ($this->_options['optional']) {
if ($calendartype->get_name() === 'gregorian') {
$image = $OUTPUT->pix_icon('i/calendar', get_string('calendar', 'calendar'), 'moodle');
$this->_elements[] = $this->createFormElement('link', 'calendar',
- null, '#', $image,
- array('class' => 'visibleifjs'));
+ null, '#', $image);
}
// If optional we add a checkbox which the user can use to turn if on
if ($this->_options['optional']) {
And I click on "Grade" "link" in the "Student 1" "table_row"
And I set the field "Grade" to "C"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I follow "Edit settings"
When I expand all fieldsets
Then I should see "Some grades have already been awarded, so the grade type and scale cannot be changed"
And I click on "Grade" "link" in the "Student 1" "table_row"
And I set the field "Grade out of 100" to "50"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I follow "Edit settings"
When I expand all fieldsets
Then I should see "Some grades have already been awarded, so the grade type cannot be changed. If you wish to change the maximum grade, you must first choose whether or not to rescale existing grades."
$DB->insert_record('license', $license);
self::reset_license_cache();
+ // Update the config setting of active licenses.
+ self::set_active_licenses();
}
/**
}
}
+ // Update the config setting of active licenses as well.
+ self::set_active_licenses();
+
} else {
throw new moodle_exception('cannotdeletecore', 'license');
}
// Interpret core license strings for internationalisation.
if ($license->custom == self::CORE_LICENSE) {
$license->fullname = get_string($license->shortname, 'license');
+ } else {
+ $license->fullname = format_string($license->fullname);
}
$licenses[$license->shortname] = $license;
}
$licenses = self::get_licenses();
foreach ($licenses as $license) {
if (in_array($license->shortname, $activelicenses)) {
- // Interpret core license strings for internationalisation.
- if (isset($license->custom) && $license->custom == self::CORE_LICENSE) {
- $license->fullname = get_string($license->shortname, 'license');
- }
$result[$license->shortname] = $license;
}
}
$heading->header = true;
}
- if ($heading->header && empty($heading->scope)) {
- $heading->scope = 'col';
+ $tagtype = 'td';
+ if ($heading->header && (string)$heading->text != '') {
+ $tagtype = 'th';
}
$heading->attributes['class'] .= ' header c' . $key;
$heading->attributes['class'] .= ' ' . $table->colclasses[$key];
}
$heading->attributes['class'] = trim($heading->attributes['class']);
- $attributes = array_merge($heading->attributes, array(
- 'style' => $table->align[$key] . $table->size[$key] . $heading->style,
- 'scope' => $heading->scope,
- 'colspan' => $heading->colspan,
- ));
+ $attributes = array_merge($heading->attributes, [
+ 'style' => $table->align[$key] . $table->size[$key] . $heading->style,
+ 'colspan' => $heading->colspan,
+ ]);
- $tagtype = 'td';
- if ($heading->header === true) {
- $tagtype = 'th';
+ if ($tagtype == 'th') {
+ $attributes['scope'] = !empty($heading->scope) ? $heading->scope : 'col';
}
+
$output .= html_writer::tag($tagtype, $heading->text, $attributes) . "\n";
}
$output .= html_writer::end_tag('tr') . "\n";
if ($this->page->pagetype == 'site-index') {
// Special case for site home page - please do not remove
return '<div class="sitelink">' .
- '<a title="Moodle" href="http://moodle.org/">' .
+ '<a title="Moodle" class="d-inline-block aalink" href="http://moodle.org/">' .
'<img src="' . $this->image_url('moodlelogo_grayhat') . '" alt="'.get_string('moodlelogo').'" /></a></div>';
} else if (!empty($CFG->target_release) && $CFG->target_release != $CFG->release) {
if (!empty($this->page->context->id)) {
$this->page->requires->js_call_amd('core/notification', 'init', array(
$this->page->context->id,
- \core\notification::fetch_as_array($this)
+ \core\notification::fetch_as_array($this),
+ isloggedin()
));
}
$footer = str_replace($this->unique_end_html_token, $this->page->requires->get_end_code(), $footer);
$url = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $courseid));
}
- $attributes = array('href'=>$url);
+ $attributes = array('href' => $url, 'class' => 'd-inline-block aabtn');
if (!$userpicture->visibletoscreenreaders) {
$attributes['tabindex'] = '-1';
$attributes['aria-hidden'] = 'true';
}
$filterset = new $filtersetclass();
+ $filterset->set_join_type($jointype);
foreach ($filters as $rawfilter) {
$filterset->add_filter_from_params(
$rawfilter['name'],
$this->sortdata = [];
foreach ($sortdata as $sortitem) {
if (!array_key_exists($sortitem['sortby'], $this->sortdata)) {
- $this->sortdata[$sortitem['sortby']] = $sortitem['sortorder'];
+ $this->sortdata[$sortitem['sortby']] = (int) $sortitem['sortorder'];
}
}
}
}
}}
{{^disabled}}
- <a href="{{url}}" class="{{classes}}" {{#attributes}}{{name}}={{#quote}}{{value}}{{/quote}} {{/attributes}}{{#showtext}}aria-labelledby="actionmenuaction-{{instance}}"{{/showtext}}>{{#icon}}{{#pix}}{{key}}, {{component}}, {{title}}{{/pix}}{{/icon}}{{#showtext}}<span class="menu-action-text" id="actionmenuaction-{{instance}}">{{{text}}}</span>{{/showtext}}</a>
+ <a href="{{url}}" class="aabtn {{classes}}" {{#attributes}}{{name}}={{#quote}}{{value}}{{/quote}} {{/attributes}}{{#showtext}}aria-labelledby="actionmenuaction-{{instance}}"{{/showtext}}>{{#icon}}{{#pix}}{{key}}, {{component}}, {{title}}{{/pix}}{{/icon}}{{#showtext}}<span class="menu-action-text" id="actionmenuaction-{{instance}}">{{{text}}}</span>{{/showtext}}</a>
{{/disabled}}
{{#disabled}}
<span class="currentlink" role="menuitem">{{#icon}}{{#pix}}{{key}},{{component}},{{title}}{{/pix}}{{/icon}}{{{text}}}</span>
}
}}
<div class="dropdown{{^secondary.items}} hidden{{/secondary.items}}">
- <a href="#" tabindex="0" class="{{triggerextraclasses}} dropdown-toggle icon-no-margin" id="action-menu-toggle-{{instance}}" aria-label="{{title}}" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" aria-controls="action-menu-{{instance}}-menu">
+ <a href="#" tabindex="0" class="d-inline-block {{triggerextraclasses}} dropdown-toggle icon-no-margin" id="action-menu-toggle-{{instance}}" aria-label="{{title}}" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" aria-controls="action-menu-{{instance}}-menu">
{{{actiontext}}}
{{{menutrigger}}}
{{#icon}}
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template core/filemanager_chooselicense
+
+ This template renders the form label and select element for choosing a license associated with a file.
+
+ Example context (json):
+ {
+ "licensehelpicon": {
+ "text": "<ul><li><a href='http://en.wikipedia.org/wiki/All_rights_reserved'>All rights reserved</a></li></ul>",
+ "alt": "Help with Choose license"
+ },
+ "columns": true
+ }
+}}
+<div class="col-form-label form-control-label px-0 {{#columns}}col-4{{/columns}}">
+ <label for="choose-license-{{uniqid}}">
+ {{#str}}chooselicense, repository{{/str}}
+ </label>
+ {{#licensehelpicon}}{{>core/help_icon}}{{/licensehelpicon}}
+</div>
+<div {{#columns}}class="col-8"{{/columns}}>
+ <select id="choose-license-{{uniqid}}" class="form-control"></select>
+</div>
{}
}}
<div class="fp-def-search form-group">
- <label class="sr-only">{{#str}}searchrepo, repository{{/str}}</label>
+ <label class="sr-only" for="reposearch">{{#str}}searchrepo, repository{{/str}}</label>
<input type="search" class="form-control" id="reposearch" name="s" placeholder="{{#str}}search, repository{{/str}}"/>
</div>
Example context (json):
{
- "helpicon": "<a class='btn ..'><i class='icon fa fa-question-circle ..'></i></a>"
+ "helpicon": "<a class='btn ..'><i class='icon fa fa-question-circle ..'></i></a>",
+ "licensehelpicon": {
+ "text": "<ul><li><a href='http://en.wikipedia.org/wiki/All_rights_reserved'>All rights reserved</a></li></ul>",
+ "alt": "Help with Choose license"
+ },
+ "columns": true
}
}}
<div class="filemanager fp-select">
</div>
<div class="fp-license form-group row mx-0">
- <label class="form-control-label col-4 px-0">{{#str}}chooselicense, repository{{/str}}</label>
- <div class="col-8 form-inline pr-0">
- <select class="custom-select form-control"></select>
- </div>
+ {{>core/filemanager_chooselicense}}
</div>
<div class="fp-path form-group row mx-0">
<label class="form-control-label col-4 px-0">{{#str}}path, repository{{/str}}</label>
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template core/filemanager_licenselinks
+
+ This template renders the window with file information/actions.
+
+ Example context (json):
+ {
+ "licenses":[
+ {
+ "fullname":"All rights reserved",
+ "source":"http:\/\/en.wikipedia.org\/wiki\/All_rights_reserved"
+ },
+ {
+ "fullname":"Public Domain",
+ "source":"http:\/\/creativecommons.org\/licenses\/publicdomain\/"
+ },
+ {
+ "fullname":"Creative Commons",
+ "source":"http:\/\/creativecommons.org\/licenses\/by\/3.0\/"
+ },
+ {
+ "fullname":"Creative Commons - NoDerivs",
+ "source":"http:\/\/creativecommons.org\/licenses\/by-nd\/3.0\/"
+ },
+ {
+ "fullname":"Creative Commons - No Commercial NoDerivs",
+ "source":"http:\/\/creativecommons.org\/licenses\/by-nc-nd\/3.0\/"
+ },
+ {
+ "fullname":"Creative Commons - No Commercial",
+ "source":"http:\/\/creativecommons.org\/licenses\/by-nc\/3.0\/"
+ },
+ {
+ "fullname":"Creative Commons - No Commercial ShareAlike",
+ "source":"http:\/\/creativecommons.org\/licenses\/by-nc-sa\/3.0\/"
+ },
+ {
+ "fullname":"Creative Commons - ShareAlike",
+ "source":"http:\/\/creativecommons.org\/licenses\/by-sa\/3.0\/"
+ }
+ ]
+ }
+}}
+<p class="mb-1">
+ {{#str}}chooselicense_help, repository{{/str}}
+</p>
+<ul>
+{{#licenses}}
+ <li>
+ <a href="{{source}}" target="_blank">{{fullname}}</a>
+ </li>
+{{/licenses}}
+{{^licenses}}
+ <li>
+ {{#str}}nolicenses, repository{{/str}}
+ </li>
+{{/licenses}}
+</ul>
<div class="fp-clear-left"></div>
</div>
<div class="fp-pathbar">
- <span class="fp-path-folder"><a class="fp-path-folder-name" href="#"></a></span>
+ <span class="fp-path-folder"><a class="fp-path-folder-name aalink" href="#"></a></span>
</div>
</div>
<div class="fp-content card"></div>
</a>
</div>
<div class="fp-btn-download">
- <a role="button" title="{{#str}}downloadselected, repository{{/str}}" class="btn btn-secondary btn-sm" href="#">
+ <a role="button" title="{{#str}}download, repository{{/str}}" class="btn btn-secondary btn-sm" href="#">
{{#pix}}a/download_all{{/pix}}
</a>
</div>
<div class="fp-btn-delete">
- <a role="button" title="{{#str}}deleteselected, repository{{/str}}" class="btn btn-secondary btn-sm" href="#">
+ <a role="button" title="{{#str}}delete{{/str}}" class="btn btn-secondary btn-sm" href="#">
{{#pix}}i/trash{{/pix}}
</a>
</div>
</div>
</div>
<div class="fp-pathbar">
- <span class="fp-path-folder"><a class="fp-path-folder-name" href="#"></a></span>
+ <span class="fp-path-folder"><a class="fp-path-folder-name aalink" href="#"></a></span>
</div>
</div>
<div class="filemanager-loading mdl-align">{{#pix}}i/loading_small{{/pix}}<span class="sr-only">{{#str}}loadinghelp{{/str}}</span></div>
<input class="form-control" type="text">
</div>
<div class="fp-setlicense form-group row">
- <label class="col-form-label">{{#str}}chooselicense, repository{{/str}}</label>
- <select class="custom-select"></select>
+ {{>core/filemanager_chooselicense}}
</div>
<div class="form-group row">
<div class="fp-select-buttons">
<label>{{#str}}author, repository{{/str}}</label>
<input type="text" class="form-control"/>
</div>
- <div class="fp-setlicense control-group">
- <label>{{#str}}chooselicense, repository{{/str}}</label>
- <select class="custom-select"></select>
+ <div class="fp-setlicense form-group">
+ {{>core/filemanager_chooselicense}}
</div>
</div>
</form>
{ "inputID": 1, "suggestionsId": 2, "selectionId": 3, "downArrowId": 4, "placeholder": "Select something" }
}}
{{#showSuggestions}}
-<div class="d-inline-block position-relative">
+<div class="d-md-inline-block mr-md-2 position-relative">
<input type="text" id="{{inputId}}" class="form-control" list="{{suggestionsId}}" placeholder="{{placeholder}}" role="combobox" aria-expanded="false" autocomplete="off" autocorrect="off" autocapitalize="off" aria-autocomplete="list" aria-owns="{{suggestionsId}} {{selectionId}}" {{#tags}}data-tags="1"{{/tags}}/>
<span class="form-autocomplete-downarrow position-absolute p-1" id="{{downArrowId}}">▼</span>
</div>
{{/showSuggestions}}
{{^showSuggestions}}
-<input type="text" id="{{inputId}}" placeholder="{{placeholder}}" role="textbox" aria-owns="{{selectionId}}" {{#tags}}data-tags="1"{{/tags}}/>
+<div class="d-md-inline-block mr-md-2">
+ <input type="text" id="{{inputId}}" class="form-control" placeholder="{{placeholder}}" role="textbox" aria-owns="{{selectionId}}" {{#tags}}data-tags="1"{{/tags}}/>
+</div>
{{/showSuggestions}}
{{#js}}
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template core/form_autocomplete_layout
+
+ Moodle template for the layout of autocomplete elements.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * data-region="form_autocomplete-input"
+ * data-region="form_autocomplete-suggestions"
+ * data-region="form_autocomplete-selection"
+
+ Context variables required for this template:
+ * none
+
+ Example context (json):
+ {}
+}}
+<div data-region="form_autocomplete-selection"></div>
+<div data-region="form_autocomplete-input"></div>
+<div data-region="form_autocomplete-suggestions"></div>
<span class="inplaceeditable inplaceeditable-{{type}}" data-inplaceeditable="1" data-component="{{component}}" data-itemtype="{{itemtype}}" data-itemid="{{itemid}}"
data-value="{{value}}" data-editlabel="{{editlabel}}" data-type="{{type}}" data-options="{{options}}">
{{^ linkeverything }}{{{displayvalue}}}{{/ linkeverything }}
- <a href="#" class="quickeditlink" data-inplaceeditablelink="1" title="{{edithint}}">
+ <a href="#" class="quickeditlink aalink" data-inplaceeditablelink="1" title="{{edithint}}">
{{# linkeverything }}{{{displayvalue}}}{{/ linkeverything }}
<span class="quickediticon visibleifjs">{{#pix}}t/editstring,core,{{{edithint}}}{{/pix}}</span>
</a>
{{< core/modal }}
{{$footer}}
- <button type="button" class="btn btn-primary" data-action="cancel">{{#str}}okay, moodle{{/str}}</button>
+ <button type="button" class="btn btn-primary" data-action="cancel">{{#str}}ok, moodle{{/str}}</button>
{{/footer}}
{{/ core/modal }}
{{#hasidentityproviders}}
<h6 class="mt-2">{{#str}} potentialidps, auth {{/str}}</h6>
- <div class="potentialidplist" class="mt-3">
+ <div class="potentialidplist mt-3">
{{#identityproviders}}
<div class="potentialidp">
<a href="{{url}}" title={{#quote}}{{name}}{{/quote}} class="btn btn-secondary btn-block">
case 'page menu' :
// This lang string was changed in app version 3.6.
selector = 'core-context-menu > button[aria-label=Info], ' +
- 'core-context-menu > button[aria-label=Information]';
+ 'core-context-menu > button[aria-label=Information], ' +
+ 'core-context-menu > button[aria-label="Display options"]';
break;
default:
return 'ERROR: Unsupported standard button type';
// Wait until the site login field appears OR the main page.
$situation = $this->spin(
function($context, $args) {
- $input = $context->getSession()->getPage()->find('xpath', '//input[@name="url"]');
- if ($input) {
+ $page = $context->getSession()->getPage();
+
+ $element = $page->find('xpath', '//page-core-login-site//input[@name="url"]');
+ if ($element) {
+ // Wait for the onboarding modal to open, if any.
+ $this->wait_for_pending_js();
+ $element = $page->find('xpath', '//page-core-login-site-onboarding');
+ if ($element) {
+ $this->i_press_in_the_app('Skip');
+ }
+
return 'login';
}
- $mainmenu = $context->getSession()->getPage()->find('xpath', '//page-core-mainmenu');
- if ($mainmenu) {
+
+ $element = $page->find('xpath', '//page-core-mainmenu');
+ if ($element) {
return 'mainpage';
}
throw new DriverException('Moodle app login URL prompt not found');
// If it's the login page, we automatically fill in the URL and leave it on the user/pass
// page. If it's the main page, we just leave it there.
if ($situation === 'login') {
- $this->i_set_the_field_in_the_app('Site address', $CFG->wwwroot);
+ $this->i_set_the_field_in_the_app('campus.example.edu', $CFG->wwwroot);
$this->i_press_in_the_app('Connect!');
}
$this->getSession());
}
}
+
+ /**
+ * Manually press enter key.
+ *
+ * @When /^I press enter/
+ * @throws DriverException
+ */
+ public function i_manually_press_enter() {
+ if (!$this->running_javascript()) {
+ throw new DriverException('Enter press step is not available with Javascript disabled');
+ }
+
+ $value = [\WebDriver\Key::ENTER];
+ $this->getSession()->getDriver()->getWebDriverSession()->activeElement()->postValue(['value' => $value]);
+ }
}
*
* @attribute yesLabel
* @type String
- * @default 'Ok'
+ * @default 'OK'
*/
yesLabel: {
validator: Y.Lang.isString,
setter: function(txt) {
if (!txt) {
- txt = 'Ok';
+ txt = 'OK';
}
return txt;
},
- value: 'Ok'
+ value: 'OK'
}
}
});
And I draw on the pdf
And I press "Save changes"
And I should see "The changes to the grade and feedback were saved"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
Given I set the field "applytoall" to "0"
And I press "Save changes"
And I should see "The changes to the grade and feedback were saved"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I log out
And I log in as "student1"
@javascript
Scenario: Submit a PDF file as a student and annotate the PDF as a teacher and all students in the group get a copy of the annotated PDF.
Given I press "Save changes"
- And I click on "Ok" "button"
+ And I click on "OK" "button"
And I am on "Course 1" course homepage
And I log out
And I log in as "student1"
And I press "Save changes"
And I wait until the page is ready
And I should see "The changes to the grade and feedback were saved"
- And I press "Ok"
+ And I press "OK"
And I follow "View a different attempt"
And I click on "Attempt 1" "radio" in the "View a different attempt" "dialogue"
And I press "View"
Scenario: A teacher can provide a feedback file when grading an assignment.
Given I set the field "applytoall" to "0"
And I press "Save changes"
- And I click on "Ok" "button"
+ And I click on "OK" "button"
And I click on "Course 1" "link" in the "[data-region=assignment-info]" "css_element"
And I log out
And I log in as "student1"
@javascript
Scenario: A teacher can provide a feedback file when grading an assignment and all students in the group will receive the file.
Given I press "Save changes"
- And I click on "Ok" "button"
+ And I click on "OK" "button"
And I click on "Course 1" "link" in the "[data-region=assignment-info]" "css_element"
And I log out
And I log in as "student1"
And I set the following fields to these values:
| Allow another attempt | 1 |
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I log out
Then I log in as "student1"
And I set the following fields to these values:
| Allow another attempt | 1 |
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I follow "Assignment: Test assignment name"
And I log out
And I log in as "student4"
| Feedback comments | I'm the teacher feedback |
And I upload "lib/tests/fixtures/empty.txt" file to "Feedback files" filemanager
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I set the field "Grade out of 100" to "50"
And I set the field "Notify students" to "0"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I set the field "Grade out of 100" to "50"
And I set the field "Notify students" to "0"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
| Feedback comments | I'm the teacher first feedback |
| Allow another attempt | Yes |
And I press "Save changes"
- And I click on "Ok" "button"
+ And I click on "OK" "button"
And I click on "Edit settings" "link"
And I log out
And I log in as "student2"
| Grade | 50 |
| Feedback comments | I'm the teacher second feedback |
And I press "Save changes"
- And I click on "Ok" "button"
+ And I click on "OK" "button"
And I click on "Edit settings" "link"
And I log out
Then I log in as "student2"
And I set the field "allocatedmarker" to "Marker 1"
And I set the field "Notify students" to "0"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I log out
When I log in as "teacher1"
And I set the field "Feedback comments" to "Great job! Lol, not really."
And I set the field "Notify students" to "0"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I should see "1 of 1"
And I set the field "Marking workflow state" to "Released"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I set the field "Marking workflow state" to "In marking"
And I set the field "Notify students" to "0"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I set the field "Grade out of 100" to "50"
And I set the field "Feedback comments" to "Great job! Lol, not really."
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I set the field "Grade out of 100" to "99.99"
And I set the field "Feedback comments" to "Even better job! Really."
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
| Grade out of 100 | 50.0 |
| Apply grades and feedback to entire group | 1 |
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I set the following fields to these values:
| Allow another attempt | 1 |
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
When I am on "Course 1" course homepage
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I set the field "Grade out of 100" to "50"
And I set the field "Feedback comments" to "Catch for us the foxes."
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I should see "Graded" in the "Student 1" "table_row"
And I set the following fields to these values:
| Outcome Test: | Excellent |
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
| Outcome Test: | Excellent |
| Apply grades and feedback to entire group | Yes |
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
| Outcome Test: | Disappointing |
| Apply grades and feedback to entire group | No |
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I click on "Grade" "link" in the "Student 1" "table_row"
And I wait until the page is ready
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
Then I should see "1" in the "Needs grading" "table_row"
| M8d skillZ! | 1337 |
| Feedback comments | I'm the teacher first feedback |
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I click on "Grade" "link" in the "Student 1" "table_row"
And I set the field "Grade out of 100" to "40"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I follow "Edit settings"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I set the field "Feedback comments" to "Great job! Lol, not really."
And I set the field "Notify students" to "0"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I set the field "Marking workflow state" to "Ready for release"
And I set the field "Notify students" to "0"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I click on "Grade" "link" in the "I'm the student's first submission" "table_row"
And I set the field "Marking workflow state" to "Released"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I set the field "Marking workflow state" to "Ready for release"
And I set the field "Notify students" to "0"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I click on "Grade" "link" in the "Student 1" "table_row"
And I set the field "Marking workflow state" to "Released"
And I press "Save changes"
- And I press "Ok"
+ And I press "OK"
And I click on "Edit settings" "link"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
<