// "courserequests" settingpage.
$temp = new admin_settingpage('courserequest', new lang_string('courserequest'));
- $temp->add(new admin_setting_configcheckbox('enablecourserequests', new lang_string('enablecourserequests', 'admin'), new lang_string('configenablecourserequests', 'admin'), 0));
- $temp->add(new admin_settings_coursecat_select('defaultrequestcategory', new lang_string('defaultrequestcategory', 'admin'), new lang_string('configdefaultrequestcategory', 'admin'), 1));
- $temp->add(new admin_setting_configcheckbox('requestcategoryselection', new lang_string('requestcategoryselection', 'admin'), new lang_string('configrequestcategoryselection', 'admin'), 0));
+ $temp->add(new admin_setting_configcheckbox('enablecourserequests',
+ new lang_string('enablecourserequests', 'admin'),
+ new lang_string('configenablecourserequests', 'admin'), 1));
+ $temp->add(new admin_settings_coursecat_select('defaultrequestcategory',
+ new lang_string('defaultrequestcategory', 'admin'),
+ new lang_string('configdefaultrequestcategory', 'admin'), 1));
+ $temp->add(new admin_setting_configcheckbox('lockrequestcategory',
+ new lang_string('lockrequestcategory', 'admin'),
+ new lang_string('configlockrequestcategory', 'admin'), 0));
$temp->add(new admin_setting_users_with_capability('courserequestnotify', new lang_string('courserequestnotify', 'admin'), new lang_string('configcourserequestnotify2', 'admin'), array(), 'moodle/site:approvecourse'));
$ADMIN->add('courses', $temp);
var expandedImage = $('<img alt="" src="' + url.imageUrl('t/expanded') + '"/>');
var collapsedImage = $('<img alt="" src="' + url.imageUrl('t/collapsed') + '"/>');
+ /*
+ * Class names to apply when expanding/collapsing nodes.
+ */
+ var CLASSES = {
+ EXPAND: 'fa-caret-right',
+ COLLAPSE: 'fa-caret-down'
+ };
+
return /** @alias module:tool_dataprivacy/expand-collapse */ {
/**
* Expand or collapse a selected node.
targetnode.removeClass('hide');
targetnode.addClass('visible');
targetnode.attr('aria-expanded', true);
- thisnode.find(':header i.fa').removeClass('fa-plus-square');
- thisnode.find(':header i.fa').addClass('fa-minus-square');
+ thisnode.find(':header i.fa').removeClass(CLASSES.EXPAND);
+ thisnode.find(':header i.fa').addClass(CLASSES.COLLAPSE);
thisnode.find(':header img.icon').attr('src', expandedImage.attr('src'));
} else {
targetnode.removeClass('visible');
targetnode.addClass('hide');
targetnode.attr('aria-expanded', false);
- thisnode.find(':header i.fa').removeClass('fa-minus-square');
- thisnode.find(':header i.fa').addClass('fa-plus-square');
+ thisnode.find(':header i.fa').removeClass(CLASSES.COLLAPSE);
+ thisnode.find(':header i.fa').addClass(CLASSES.EXPAND);
thisnode.find(':header img.icon').attr('src', collapsedImage.attr('src'));
}
},
expandCollapseAll: function(nextstate) {
var currentstate = (nextstate == 'visible') ? 'hide' : 'visible';
var ariaexpandedstate = (nextstate == 'visible') ? true : false;
- var iconclassnow = (nextstate == 'visible') ? 'fa-plus-square' : 'fa-minus-square';
- var iconclassnext = (nextstate == 'visible') ? 'fa-minus-square' : 'fa-plus-square';
+ var iconclassnow = (nextstate == 'visible') ? CLASSES.EXPAND : CLASSES.COLLAPSE;
+ var iconclassnext = (nextstate == 'visible') ? CLASSES.COLLAPSE : CLASSES.EXPAND;
var imagenow = (nextstate == 'visible') ? expandedImage.attr('src') : collapsedImage.attr('src');
$('.' + currentstate).each(function() {
$(this).removeClass(currentstate);
--- /dev/null
+username,firstname,lastname,email
+student1,Student,One,s1@example.com
+student2,Student,Two,s2@example.com
+student3,Student,Three,s3@example.com
\ No newline at end of file
$string['encoding'] = 'Encoding';
$string['errormnetadd'] = 'Can not add remote users';
$string['errors'] = 'Errors';
+$string['examplecsv'] = 'Example text file';
+$string['examplecsv_help'] = 'To use the example text file, download it then open it with a text or spreadsheet editor. Leave the first line unchanged, then edit the following lines (records) and add your user data, adding more lines as necessary. Save the file as CSV then upload it.
+
+The example text file may also be used for testing, as you are able to preview user data and can choose to cancel the action before user accounts are created.';
$string['invalidupdatetype'] = 'This option cannot be selected with the chosen upload type.';
$string['invaliduserdata'] = 'Invalid data detected for user {$a} and it has been automatically cleaned.';
$string['invalidtheme'] = 'Theme "{$a}" is not installed and will be ignored.';
* Each line of the file contains one record
* Each record is a series of data separated by commas (or other delimiters)
* The first record contains a list of fieldnames defining the format of the rest of the file
-* Required fieldnames are username, password, firstname, lastname, email';
+* Required fieldnames are username, password, firstname, lastname, email
+
+<a href="https://docs.moodle.org/en/Upload_users" target="_blank">More help</a>';
$string['uploaduserspreview'] = 'Upload users preview';
$string['uploadusersresult'] = 'Upload users results';
$string['uploaduser:uploaduserpictures'] = 'Upload user pictures';
$mform->addElement('header', 'settingsheader', get_string('upload'));
+ $url = new moodle_url('example.csv');
+ $link = html_writer::link($url, 'example.csv');
+ $mform->addElement('static', 'examplecsv', get_string('examplecsv', 'tool_uploaduser'), $link);
+ $mform->addHelpButton('examplecsv', 'examplecsv', 'tool_uploaduser');
+
$mform->addElement('filepicker', 'userfile', get_string('file'));
$mform->addRule('userfile', null, 'required');
* Logs in the user. There should exist a user with the same value as username and password.
*
* @Given /^I log in as "(?P<username_string>(?:[^"]|\\")*)"$/
+ * @param string $username the user to log in as.
+ * @param moodle_url|null $wantsurl optional, URL to go to after logging in.
*/
- public function i_log_in_as($username) {
- // In the mobile app the required tasks are different.
+ public function i_log_in_as(string $username, moodle_url $wantsurl = null) {
+ // In the mobile app the required tasks are different (does not support $wantsurl).
if ($this->is_in_app()) {
$this->execute('behat_app::login', [$username]);
return;
}
+ $loginurl = new moodle_url('/login/index.php');
+ if ($wantsurl !== null) {
+ $loginurl->param('wantsurl', $wantsurl->out_as_local_url());
+ }
+
// Visit login page.
- $this->getSession()->visit($this->locate_path('login/index.php'));
+ $this->getSession()->visit($this->locate_path($loginurl->out_as_local_url()));
// Enter username and password.
$this->execute('behat_forms::i_set_the_field_to', array('Username', $this->escape($username)));
--- /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/>.
+
+/**
+ * Contains upgrade and install functions for badges.
+ *
+ * @package core_badges
+ * @copyright 2019 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Called on install or upgrade to create default list of backpacks a user can connect to.
+ * Don't use the global defines from badgeslib because this is for install/upgrade.
+ *
+ * @return void
+ */
+function badges_install_default_backpacks() {
+ global $DB;
+
+ $record = new stdClass();
+ $record->backpackweburl = 'https://backpack.openbadges.org';
+ $record->backpackapiurl = 'https://backpack.openbadges.org';
+ $record->apiversion = 1;
+ $record->sortorder = 0;
+ $record->password = '';
+
+ if (!($bp = $DB->get_record('badge_external_backpack', array('backpackapiurl' => $record->backpackapiurl)))) {
+ $bpid = $DB->insert_record('badge_external_backpack', $record);
+ } else {
+ $bpid = $bp->id;
+ }
+ set_config('badges_site_backpack', $bpid);
+
+ // All existing backpacks default to V1.
+ $DB->set_field('badge_backpack', 'externalbackpackid', $bpid);
+
+ $record = new stdClass();
+ $record->backpackapiurl = 'https://api.badgr.io/v2';
+ $record->backpackweburl = 'https://badgr.io';
+ $record->apiversion = 2;
+ $record->sortorder = 1;
+ $record->password = '';
+
+ if (!$DB->record_exists('badge_external_backpack', array('backpackapiurl' => $record->backpackapiurl))) {
+ $DB->insert_record('badge_external_backpack', $record);
+ }
+
+}
+
return ($a->timemodified > $b->timemodified) ? -1 : 1;
});
- $formattedcourses = array_map(function($favourite) use ($renderer) {
+ $formattedcourses = array();
+ foreach ($favourites as $favourite) {
$course = get_course($favourite->itemid);
$context = \context_course::instance($favourite->itemid);
-
- $exporter = new course_summary_exporter($course, ['context' => $context, 'isfavourite' => true]);
- return $exporter->export($renderer);
- }, $favourites);
+ $canviewhiddencourses = has_capability('moodle/course:viewhiddencourses', $context);
+
+ if ($course->visible || $canviewhiddencourses) {
+ $exporter = new course_summary_exporter($course, ['context' => $context, 'isfavourite' => true]);
+ $formattedcourse = $exporter->export($renderer);
+ $formattedcourses[] = $formattedcourse;
+ }
+ }
return $formattedcourses;
}
}}
<div class="border-bottom pb-2">
{{#eventsbyday}}
- <h5 class="h6 mt-3 mb-0 {{#past}}text-danger{{/past}}">{{#userdate}} {{dayTimestamp}}, {{#str}} strftimedayshort, core_langconfig {{/str}} {{/userdate}}</h5>
+ <h5 class="h6 mt-3 mb-0 {{#past}}text-danger{{/past}}">{{#userdate}} {{dayTimestamp}}, {{#str}} strftimedaydate, core_langconfig {{/str}} {{/userdate}}</h5>
{{> block_timeline/event-list-items }}
{{/eventsbyday}}
</div>
\ No newline at end of file
$params = array_merge($inparams, ['userid' => $userid]);
$associds = $DB->get_fieldset_sql($sql, $params);
- list($insql, $inparams) = $DB->get_in_or_equal($associds, SQL_PARAMS_NAMED, 'param', true);
- $DB->delete_records_select('blog_association', "id $insql", $inparams);
+ $DB->delete_records_list('blog_association', 'id', $associds);
}
}
$this->assertTrue($DB->record_exists('post', ['courseid' => $c1->id, 'userid' => $u1->id, 'module' => 'notes']));
}
+ /**
+ * Test provider delete_data_for_user with a context that contains no entries
+ *
+ * @return void
+ */
+ public function test_delete_data_for_user_empty_context() {
+ global $DB;
+
+ $user = $this->getDataGenerator()->create_user();
+ $course = $this->getDataGenerator()->create_course();
+ $context = context_course::instance($course->id);
+
+ // Create a blog entry for user, associated with course.
+ $entry = new blog_entry($this->create_post(['userid' => $user->id, 'courseid' => $course->id])->id);
+ $entry->add_association($context->id);
+
+ // Generate list of contexts for user.
+ $contexts = provider::get_contexts_for_userid($user->id);
+ $this->assertContains($context->id, $contexts->get_contextids());
+
+ // Now delete the blog entry.
+ $entry->delete();
+
+ // Try to delete user data using contexts obtained prior to entry deletion.
+ $contextlist = new approved_contextlist($user, 'core_blog', $contexts->get_contextids());
+ provider::delete_data_for_user($contextlist);
+
+ // Sanity check to ensure blog_associations is really empty.
+ $this->assertEmpty($DB->get_records('blog_association', ['contextid' => $context->id]));
+ }
+
public function test_delete_data_for_all_users_in_context() {
global $DB;
//
// $CFG->uninstallclionly = true;
//
+//
+// Customise question bank display
+//
+// The display of Moodle's question bank is made up of a number of columns.
+// You can customise this display by giving a comma-separated list of column class
+// names here. Each class must be a subclass of \core_question\bank\column_base.
+// For example you might define a class like
+// class \local_qbank_extensions\my_column extends \core_question\bank\column_base
+// in a local plugin, then add it to the list here. At the time of writing,
+// the default question bank display is equivalent to the following, but you might like
+// to check the latest default in question/classes/bank/view.php before setting this.
+//
+// $CFG->questionbankcolumns = 'checkbox_column,question_type_column,'
+// . 'question_name_idnumber_tags_column,'
+// . 'tags_action_column,edit_action_column,copy_action_column,'
+// . 'preview_action_column,delete_action_column,export_xml_action_column,'
+// . 'creator_name_column,modifier_name_column,edit_menu_column';
+//
+// Forum summary report
+//
+// In order for the forum summary report to calculate word count and character count data, those details are now stored
+// for each post in the database when posts are created or updated. For posts that existed prior to a Moodle 3.8 upgrade,
+// these are calculated by the refresh_forum_post_counts ad-hoc task in chunks of 5000 posts per batch by default.
+// That default can be overridden by setting an integer value for $CFG->forumpostcountchunksize.
+//
+// $CFG->forumpostcountchunksize = 5000;
+//
//=========================================================================
// 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
//=========================================================================
return get_string('coursenotyetstarted', 'course');
}
+ if (!$fortraining && !$course->get_course_data()->visible) {
+ return get_string('hiddenfromstudents');
+ }
+
if (!$this->students = $course->get_students()) {
return get_string('nocoursestudents', 'course');
}
* @return bool
*/
public function can_request_course() {
- global $CFG;
- if (empty($CFG->enablecourserequests) || $this->id != $CFG->defaultrequestcategory) {
- return false;
- }
- return !$this->can_create_course() && has_capability('moodle/course:request', $this->get_context());
+ return course_request::can_request($this->get_context());
}
/**
}
if ($category->can_request_course()) {
// Request a new course.
- $url = new moodle_url('/course/request.php', array('return' => 'management'));
+ $url = new moodle_url('/course/request.php', array('category' => $category->id, 'return' => 'management'));
$actions[] = html_writer::link($url, get_string('requestcourse'));
}
if ($category->can_resort_courses()) {
}
/**
- * Return structure for edit_module()
+ * Return structure for get_module()
*
* @since Moodle 3.3
* @return external_description
/**
* Print the buttons relating to course requests.
*
- * @param object $context current page context.
+ * @param context $context current page context.
*/
function print_course_request_buttons($context) {
global $CFG, $DB, $OUTPUT;
if (empty($CFG->enablecourserequests)) {
return;
}
- if (!has_capability('moodle/course:create', $context) && has_capability('moodle/course:request', $context)) {
- /// Print a button to request a new course
- echo $OUTPUT->single_button(new moodle_url('/course/request.php'), get_string('requestcourse'), 'get');
+ if (course_request::can_request($context)) {
+ // Print a button to request a new course.
+ $params = [];
+ if ($context instanceof context_coursecat) {
+ $params['category'] = $context->instanceid;
+ }
+ echo $OUTPUT->single_button(new moodle_url('/course/request.php', $params),
+ get_string('requestcourse'), 'get');
}
/// Print a button to manage pending requests
if (has_capability('moodle/site:approvecourse', $context)) {
$data->requester = $USER->id;
// Setting the default category if none set.
- if (empty($data->category) || empty($CFG->requestcategoryselection)) {
+ if (empty($data->category) || !empty($CFG->lockrequestcategory)) {
$data->category = $CFG->defaultrequestcategory;
}
return $this->properties->collision;
}
+ /**
+ * Checks user capability to approve a requested course
+ *
+ * If course was requested without category for some reason (might happen if $CFG->defaultrequestcategory is
+ * misconfigured), we check capabilities 'moodle/site:approvecourse' and 'moodle/course:changecategory'.
+ *
+ * @return bool
+ */
+ public function can_approve() {
+ global $CFG;
+ $category = null;
+ if ($this->properties->category) {
+ $category = core_course_category::get($this->properties->category, IGNORE_MISSING);
+ } else if ($CFG->defaultrequestcategory) {
+ $category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING);
+ }
+ if ($category) {
+ return has_capability('moodle/site:approvecourse', $category->get_context());
+ }
+
+ // We can not determine the context where the course should be created. The approver should have
+ // both capabilities to approve courses and change course category in the system context.
+ return has_all_capabilities(['moodle/site:approvecourse', 'moodle/course:changecategory'], context_system::instance());
+ }
+
/**
* Returns the category where this course request should be created
*
*/
public function get_category() {
global $CFG;
- // If the category is not set, if the current user does not have the rights to change the category, or if the
- // category does not exist, we set the default category to the course to be approved.
- // The system level is used because the capability moodle/site:approvecourse is based on a system level.
- if (empty($this->properties->category) || !has_capability('moodle/course:changecategory', context_system::instance()) ||
- (!$category = core_course_category::get($this->properties->category, IGNORE_MISSING, true))) {
- $category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING, true);
- }
- if (!$category) {
- $category = core_course_category::get_default();
+ if ($this->properties->category && ($category = core_course_category::get($this->properties->category, IGNORE_MISSING))) {
+ return $category;
+ } else if ($CFG->defaultrequestcategory &&
+ ($category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING))) {
+ return $category;
+ } else {
+ return core_course_category::get_default();
}
- return $category;
}
/**
$eventdata->notification = 1;
message_send($eventdata);
}
+
+ /**
+ * Checks if current user can request a course in this context
+ *
+ * @param context $context
+ * @return bool
+ */
+ public static function can_request(context $context) {
+ global $CFG;
+ if (empty($CFG->enablecourserequests)) {
+ return false;
+ }
+ if (has_capability('moodle/course:create', $context)) {
+ return false;
+ }
+
+ if ($context instanceof context_system) {
+ $defaultcontext = context_coursecat::instance($CFG->defaultrequestcategory, IGNORE_MISSING);
+ return $defaultcontext &&
+ has_capability('moodle/course:request', $defaultcontext);
+ } else if ($context instanceof context_coursecat) {
+ if (!$CFG->lockrequestcategory || $CFG->defaultrequestcategory == $context->instanceid) {
+ return has_capability('moodle/course:request', $context);
+ }
+ }
+ return false;
+ }
}
/**
$reject = optional_param('reject', 0, PARAM_INT);
$baseurl = $CFG->wwwroot . '/course/pending.php';
-admin_externalpage_setup('coursespending');
+$context = context_system::instance();
+if (has_capability('moodle/site:approvecourse', $context)) {
+ // Similar to course management capabilities, if user has approve capability in system context
+ // we add the link to the admin menu. Otherwise we check if user has capability anywhere.
+ admin_externalpage_setup('coursespending');
+} else {
+ require_login(null, false);
+ $categories = core_course_category::make_categories_list('moodle/site:approvecourse');
+ if (!$categories) {
+ require_capability('moodle/site:approvecourse', $context);
+ }
+ $PAGE->set_context($context);
+ $PAGE->set_url(new moodle_url('/course/pending.php'));
+}
/// Process approval of a course.
if (!empty($approve) and confirm_sesskey()) {
$courseid = $course->approve();
if ($courseid !== false) {
- redirect(new moodle_url('/course/edit.php', ['id' => $courseid, 'returnto' => 'pending']));
+ if (has_capability('moodle/course:update', context_course::instance($courseid))) {
+ redirect(new moodle_url('/course/edit.php', ['id' => $courseid, 'returnto' => 'pending']));
+ } else {
+ redirect(new moodle_url('/course/view.php', ['id' => $courseid]));
+ }
} else {
print_error('courseapprovedfailed');
}
// Check here for shortname collisions and warn about them.
$course->check_shortname_collision();
+ if (!$course->can_approve()) {
+ continue;
+ }
$category = $course->get_category();
$row = array();
// Where we came from. Used in a number of redirects.
$url = new moodle_url('/course/request.php');
$return = optional_param('return', null, PARAM_ALPHANUMEXT);
+$categoryid = optional_param('category', null, PARAM_INT);
if ($return === 'management') {
$url->param('return', $return);
$returnurl = new moodle_url('/course/management.php', array('categoryid' => $CFG->defaultrequestcategory));
if (empty($CFG->enablecourserequests)) {
print_error('courserequestdisabled', '', $returnurl);
}
-$context = context_system::instance();
+
+if ($CFG->lockrequestcategory) {
+ // Course request category is locked, user will always request in the default request category.
+ $categoryid = null;
+} else if (!$categoryid) {
+ // Category selection is enabled but category is not specified.
+ // Find a category where user has capability to request courses (preferably the default category).
+ $list = core_course_category::make_categories_list('moodle/course:request');
+ $categoryid = array_key_exists($CFG->defaultrequestcategory, $list) ? $CFG->defaultrequestcategory : key($list);
+}
+
+$context = context_coursecat::instance($categoryid ?: $CFG->defaultrequestcategory);
$PAGE->set_context($context);
require_capability('moodle/course:request', $context);
// Set up the form.
-$data = course_request::prepare();
+$data = $categoryid ? (object)['category' => $categoryid] : null;
+$data = course_request::prepare($data);
$requestform = new course_request_form($url);
$requestform->set_data($data);
$mform->addRule('shortname', get_string('missingshortname'), 'required', null, 'client');
$mform->setType('shortname', PARAM_TEXT);
- if (!empty($CFG->requestcategoryselection)) {
- $displaylist = core_course_category::make_categories_list();
+ if (empty($CFG->lockrequestcategory)) {
+ $displaylist = core_course_category::make_categories_list('moodle/course:request');
$mform->addElement('select', 'category', get_string('coursecategory'), $displaylist);
$mform->setDefault('category', $CFG->defaultrequestcategory);
$mform->addHelpButton('category', 'coursecategory');
--- /dev/null
+@core @core_course
+Feature: Users can request and approve courses
+ As a moodle admin
+ In order to improve course creation process
+ I need to be able to enable course approval
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | user1 | User | 1 | user1@example.com |
+ | user2 | User | 2 | user2@example.com |
+ | user3 | User | 3 | user3@example.com |
+
+ Scenario: Simple course request workflow
+ Given the following "system role assigns" exist:
+ | user | course | role |
+ | user2 | Acceptance test site | manager |
+ Given I log in as "admin"
+ And I set the following administration settings values:
+ | lockrequestcategory | 1 |
+ And I set the following system permissions of "Authenticated user" role:
+ | capability | permission |
+ | moodle/course:request | Allow |
+ And I log out
+ When I log in as "user1"
+ And I am on course index
+ And I press "Request a course"
+ And I set the following fields to these values:
+ | Course full name | My new course |
+ | Course short name | Mynewcourse |
+ | Supporting information | pretty please |
+ And I press "Request a course"
+ And I should see "Your course request has been saved successfully."
+ And I press "Continue"
+ And I am on course index
+ And I should not see "My new course"
+ And I log out
+ And I log in as "user2"
+ And I am on course index
+ And I press "Courses pending approval"
+ And I should see "Miscellaneous" in the "My new course" "table_row"
+ And I click on "Approve" "button" in the "My new course" "table_row"
+ And I press "Save and return"
+ And I should see "There are no courses pending approval"
+ And I press "Back to course listing"
+ And I should see "My new course"
+ And I log out
+ And I log in as "user1"
+ And I am on course index
+ And I follow "My new course"
+ And I navigate to course participants
+ And I should see "Teacher" in the "User 1" "table_row"
+ And I log out
+
+ Scenario: Course request with category selection
+ Given the following "categories" exist:
+ | name | category | idnumber |
+ | Science category | 0 | SCI |
+ | English category | 0 | ENG |
+ | Other category | 0 | MISC |
+ Given the following "roles" exist:
+ | name | shortname | description | archetype |
+ | Course requestor | courserequestor | My custom role 1 | |
+ And the following "role assigns" exist:
+ | user | role | contextlevel | reference |
+ | user1 | courserequestor | Category | SCI |
+ | user1 | courserequestor | Category | ENG |
+ | user2 | manager | Category | SCI |
+ | user3 | manager | Category | ENG |
+ Given I log in as "admin"
+ And I set the following system permissions of "Course requestor" role:
+ | capability | permission |
+ | moodle/course:request | Allow |
+ And I log out
+ And I log in as "user1"
+ And I am on course index
+ And I follow "English category"
+ And I press "Request a course"
+ And the field "Course category" matches value "English category"
+ And I set the following fields to these values:
+ | Course full name | My new course |
+ | Course short name | Mynewcourse |
+ | Supporting information | pretty please |
+ And I press "Request a course"
+ And I log out
+ And I log in as "user2"
+ And I am on course index
+ And I follow "English category"
+ And "Courses pending approval" "button" should not exist
+ And I am on course index
+ And I follow "Science category"
+ And I press "Courses pending approval"
+ And I should not see "Mynewcourse"
+ And I press "Back to course listing"
+ And I log out
+ And I log in as "user3"
+ And I am on course index
+ And I follow "English category"
+ And I press "Courses pending approval"
+ And I should see "English category" in the "Mynewcourse" "table_row"
+ And I click on "Approve" "button" in the "Mynewcourse" "table_row"
+ And I press "Save and return"
+ And I am on course index
+ And I follow "English category"
+ And I should see "My new course"
+ And I log out
course_delete_module($moduleinstances[$indextodelete]->cmid, true); // Try to delete the instance asynchronously.
$this->assertEquals($expected, course_modules_pending_deletion($course->id, $gradable));
}
+
+ /**
+ * Tests for the course_request::can_request
+ */
+ public function test_can_request_course() {
+ global $CFG, $DB;
+ $this->resetAfterTest();
+
+ $user = $this->getDataGenerator()->create_user();
+ $cat1 = $CFG->defaultrequestcategory;
+ $cat2 = $this->getDataGenerator()->create_category()->id;
+ $cat3 = $this->getDataGenerator()->create_category()->id;
+ $context1 = context_coursecat::instance($cat1);
+ $context2 = context_coursecat::instance($cat2);
+ $context3 = context_coursecat::instance($cat3);
+ $this->setUser($user);
+
+ // By default users don't have capability to request courses.
+ $this->assertFalse(course_request::can_request(context_system::instance()));
+ $this->assertFalse(course_request::can_request($context1));
+ $this->assertFalse(course_request::can_request($context2));
+ $this->assertFalse(course_request::can_request($context3));
+
+ // Allow for the 'user' role the capability to request courses.
+ $userroleid = $DB->get_field('role', 'id', ['shortname' => 'user']);
+ assign_capability('moodle/course:request', CAP_ALLOW, $userroleid,
+ context_system::instance()->id);
+ accesslib_clear_all_caches_for_unit_testing();
+
+ // Lock category selection.
+ $CFG->lockrequestcategory = 1;
+
+ // Now user can only request course in the default category or in system context.
+ $this->assertTrue(course_request::can_request(context_system::instance()));
+ $this->assertTrue(course_request::can_request($context1));
+ $this->assertFalse(course_request::can_request($context2));
+ $this->assertFalse(course_request::can_request($context3));
+
+ // Enable category selection. User can request course anywhere.
+ $CFG->lockrequestcategory = 0;
+ $this->assertTrue(course_request::can_request(context_system::instance()));
+ $this->assertTrue(course_request::can_request($context1));
+ $this->assertTrue(course_request::can_request($context2));
+ $this->assertTrue(course_request::can_request($context3));
+
+ // Remove cap from cat2.
+ $roleid = create_role('Test role', 'testrole', 'Test role description');
+ assign_capability('moodle/course:request', CAP_PROHIBIT, $roleid,
+ $context2->id, true);
+ role_assign($roleid, $user->id, $context2->id);
+ accesslib_clear_all_caches_for_unit_testing();
+
+ $this->assertTrue(course_request::can_request(context_system::instance()));
+ $this->assertTrue(course_request::can_request($context1));
+ $this->assertFalse(course_request::can_request($context2));
+ $this->assertTrue(course_request::can_request($context3));
+
+ // Disable course request functionality.
+ $CFG->enablecourserequests = false;
+ $this->assertFalse(course_request::can_request(context_system::instance()));
+ $this->assertFalse(course_request::can_request($context1));
+ $this->assertFalse(course_request::can_request($context2));
+ $this->assertFalse(course_request::can_request($context3));
+ }
+
+ /**
+ * Tests for the course_request::can_approve
+ */
+ public function test_can_approve_course_request() {
+ global $CFG;
+ $this->resetAfterTest();
+
+ $requestor = $this->getDataGenerator()->create_user();
+ $user = $this->getDataGenerator()->create_user();
+ $cat1 = $CFG->defaultrequestcategory;
+ $cat2 = $this->getDataGenerator()->create_category()->id;
+ $cat3 = $this->getDataGenerator()->create_category()->id;
+
+ // Enable course requests. Default 'user' role has capability to request courses.
+ $CFG->enablecourserequests = true;
+ $CFG->lockrequestcategory = 0;
+ $this->setUser($requestor);
+ $requestdata = ['summary_editor' => ['text' => '', 'format' => 0], 'name' => 'Req', 'reason' => 'test'];
+ $request1 = course_request::create((object)($requestdata));
+ $request2 = course_request::create((object)($requestdata + ['category' => $cat2]));
+ $request3 = course_request::create((object)($requestdata + ['category' => $cat3]));
+
+ $this->setUser($user);
+ // Add capability to approve courses.
+ $roleid = create_role('Test role', 'testrole', 'Test role description');
+ assign_capability('moodle/site:approvecourse', CAP_ALLOW, $roleid,
+ context_system::instance()->id, true);
+ role_assign($roleid, $user->id, context_coursecat::instance($cat2)->id);
+ accesslib_clear_all_caches_for_unit_testing();
+
+ $this->assertFalse($request1->can_approve());
+ $this->assertTrue($request2->can_approve());
+ $this->assertFalse($request3->can_approve());
+
+ // Delete category where course was requested. Now only site-wide manager can approve it.
+ core_course_category::get($cat2, MUST_EXIST, true)->delete_full(false);
+ $this->assertFalse($request2->can_approve());
+
+ $this->setAdminUser();
+ $this->assertTrue($request2->can_approve());
+ }
}
$defaultcategory = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
set_config('enablecourserequests', 1);
- set_config('requestcategoryselection', 0);
+ set_config('lockrequestcategory', 1);
set_config('defaultrequestcategory', $defaultcategory);
// Create some categories.
// Request with category different than default and category selection allowed.
set_config('defaultrequestcategory', $cat3->id);
- set_config('requestcategoryselection', 1);
+ set_config('lockrequestcategory', 0);
$data->category = $cat1->id;
$cr = course_request::create($data);
$this->assertEquals($cat1->id, $cr->category);
$defaultcategory = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
set_config('enablecourserequests', 1);
- set_config('requestcategoryselection', 0);
+ set_config('lockrequestcategory', 1);
set_config('defaultrequestcategory', $defaultcategory);
// Create some categories.
$cat1 = $this->getDataGenerator()->create_category();
$cat2 = $this->getDataGenerator()->create_category();
+ // Create a user and allow course requests for him.
$requester = $this->getDataGenerator()->create_user();
+ $roleid = create_role('Course requestor role', 'courserequestor', '');
+ assign_capability('moodle/course:request', CAP_ALLOW, $roleid,
+ context_system::instance()->id);
+ role_assign($roleid, $requester->id, context_system::instance()->id);
+ accesslib_clear_all_caches_for_unit_testing();
$data = new stdClass();
$data->fullname = 'Həllo World!';
$this->assertEquals($defaultcategory, $course->category);
// Test with category.
- set_config('requestcategoryselection', 1);
+ set_config('lockrequestcategory', 0);
set_config('defaultrequestcategory', $cat2->id);
$data->shortname .= ' 2nd';
$data->category = $cat1->id;
$this->setAdminUser();
set_config('enablecourserequests', 1);
- set_config('requestcategoryselection', 0);
+ set_config('lockrequestcategory', 1);
set_config('defaultrequestcategory', $DB->get_field_select('course_categories', "MIN(id)", "parent=0"));
+ // Create a user and allow course requests for him.
$requester = $this->getDataGenerator()->create_user();
+ $roleid = create_role('Course requestor role', 'courserequestor', '');
+ assign_capability('moodle/course:request', CAP_ALLOW, $roleid,
+ context_system::instance()->id);
+ role_assign($roleid, $requester->id, context_system::instance()->id);
+ accesslib_clear_all_caches_for_unit_testing();
$data = new stdClass();
$data->fullname = 'Həllo World!';
],
'isvalid' => get_string('completionnotenabledforcourse', 'completion')
],
+ 'coursehiddentraining' => [
+ 'params' => [
+ 'enablecompletion' => 1,
+ 'startdate' => mktime(0, 0, 0, $month - 1, 24, $year - 1),
+ 'enddate' => mktime(0, 0, 0, $month - 1, 23, $year),
+ 'students' => true,
+ 'visible' => '0',
+ ],
+ 'isvalid' => true,
+ ],
+ 'coursehiddenprediction' => [
+ 'params' => [
+ 'enablecompletion' => 1,
+ 'startdate' => mktime(0, 0, 0, $month - 1, 24, $year),
+ 'enddate' => mktime(0, 0, 0, $month - 1, 23, $year + 1),
+ 'students' => true,
+ 'visible' => '0',
+ ],
+ 'isvalid' => get_string('hiddenfromstudents'),
+ 'fortraining' => false
+ ],
];
}
$string['configdebugpageinfo'] = 'Enable if you want page information printed in page footer.';
$string['configdebugvalidators'] = 'Enable if you want to have links to external validator servers in page footer. You may need to create new user with username <em>w3cvalidator</em>, and enable guest access. These changes may allow unauthorized access to server, do not enable on production sites!';
$string['configdefaulthomepage'] = 'This determines the first link in the navigation for logged-in users.';
-$string['configdefaultrequestcategory'] = 'Courses requested by users will be automatically placed in this category.';
+$string['configdefaultrequestcategory'] = 'Courses requested by users will be placed in this category if the category is not specified.';
$string['configdefaultrequestedcategory'] = 'Default category to put courses that were requested into, if they\'re approved.';
$string['configdefaultuserroleid'] = 'All logged in users will be given the capabilities of the role you specify here, at the site level, in ADDITION to any other roles they may have been given. The default is the Authenticated user role. Note that this will not conflict with other roles they have unless you prohibit capabilities, it just ensures that all users have capabilities that are not assignable at the course level (eg post blog entries, manage own calendar, etc).';
$string['configdeleteincompleteusers'] = 'After this period, any account without the first name, last name or email field filled in is deleted.';
$string['configemailsubjectprefix'] = 'Text to be prefixed to the subject line of all outgoing mail.';
$string['configenablecalendarexport'] = 'Enable exporting or subscribing to calendars.';
$string['configenablecomments'] = 'Enable comments';
-$string['configenablecourserequests'] = 'This will allow any user to request a course be created.';
+$string['configenablecourserequests'] = 'Enable course request functionality. Users with capability to request courses but without capability to create courses will be able to request courses.';
$string['configenablemobilewebservice'] = 'Enable mobile service for the official Moodle app or other app requesting it. For more information, read the {$a}';
$string['configenablerssfeeds'] = 'If enabled, RSS feeds are generated by various features across the site, such as blogs, forums, database activities and glossaries. Note that RSS feeds also need to be enabled for the particular activity modules.';
$string['configenablerssfeedsdisabled'] = 'It is not available because RSS feeds are disabled in all the Site. To enable them, go to the Variables settings under Admin Configuration.';
$string['configlangmenu'] = 'Choose whether or not you want to display the general-purpose language menu on the home page, login page etc. This does not affect the user\'s ability to set the preferred language in their own profile.';
$string['configlatinexcelexport'] = 'Choose the encoding for Excel exports.';
$string['configlocale'] = 'Choose a sitewide locale - this will override the format and language of dates for all language packs (though names of days in calendar are not affected). You need to have this locale data installed on your operating system (eg for linux en_US.UTF-8 or es_ES.UTF-8). In most cases this field should be left blank.';
+$string['configlockrequestcategory'] = 'Only allow course requests in the default course request category. This is a legacy setting, it is better not to use it but instead assign capability to request courses in the appropriate course category context';
$string['configloglifetime'] = 'This specifies the length of time you want to keep logs about user activity. Logs that are older than this age are automatically deleted. It is best to keep logs as long as possible, in case you need them, but if you have a very busy server and are experiencing performance problems, then you may want to lower the log lifetime. Values lower than 30 are not recommended because statistics may not work properly.';
$string['configlookahead'] = 'Days to look ahead';
$string['configmailnewline'] = 'Newline characters used in mail messages. CRLF is required according to RFC 822bis, some mail servers do automatic conversion from LF to CRLF, other mail servers do incorrect conversion from CRLF to CRCRLF, yet others reject mails with bare LF (qmail for example). Try changing this setting if you are having problems with undelivered emails or double newlines.';
$string['configproxyuser'] = 'Username needed to access internet through proxy if required, empty if none (PHP cURL extension required).';
$string['configrecaptchaprivatekey'] = 'String of characters (secret key) used to communicate between your Moodle server and the recaptcha server. ReCAPTCHA keys can be obtained from <a target="_blank" href="https://www.google.com/recaptcha">Google reCAPTCHA</a>.';
$string['configrecaptchapublickey'] = 'String of characters (site key) used to display the reCAPTCHA element in the signup form. ReCAPTCHA keys can be obtained from <a target="_blank" href="https://www.google.com/recaptcha">Google reCAPTCHA</a>.';
-$string['configrequestcategoryselection'] = 'Allow the selection of a category when requesting a course.';
$string['configrequestedstudentname'] = 'Word for student used in requested courses';
$string['configrequestedstudentsname'] = 'Word for students used in requested courses';
$string['configrequestedteachername'] = 'Word for teacher used in requested courses';
$string['lockoutthreshold_desc'] = 'Select number of failed login attempts that result in account lockout. This feature may be abused in denial of service attacks.';
$string['lockoutwindow'] = 'Account lockout observation window';
$string['lockoutwindow_desc'] = 'Observation time for lockout threshold, if there are no failed attempts the threshold counter is reset after this time.';
+$string['lockrequestcategory'] = 'Lock category for the course requests';
$string['log'] = 'Logs';
$string['logguests'] = 'Log guest access';
$string['logguests_help'] = 'This setting enables logging of actions by guest account and not logged in users. High profile sites may want to disable this logging for performance reasons. It is recommended to keep this setting enabled on production sites.';
$string['purgeselectedcachesfinished'] = 'The selected caches were purged.';
$string['purgetemplates'] = 'Templates';
$string['purgethemecache'] = 'Themes';
-$string['requestcategoryselection'] = 'Enable category selection';
$string['restorecourse'] = 'Restore course';
$string['restorernewroleid'] = 'Restorers\' role in courses';
$string['restorernewroleid_help'] = 'If the user does not already have the permission to manage the newly restored course, the user is automatically assigned this role and enrolled if necessary. Select "None" if you do not want restorers to be able to manage every restored course.';
$string['registermoodleorgli1'] = 'You are added to a low-volume mailing list for important notifications such as security alerts and new releases of Moodle.';
$string['registermoodleorgli2'] = 'Statistics about your site will be added to the {$a} of the worldwide Moodle community.';
$string['registerwithmoodleorg'] = 'Register your site';
+$string['configrequestcategoryselection'] = 'Allow the selection of a category when requesting a course.';
+$string['requestcategoryselection'] = 'Enable category selection';
registersite,core_hub
updatesite,core_hub
unregisterexplained,core_hub
+configrequestcategoryselection,core_admin
+requestcategoryselection,core_admin
\ No newline at end of file
$string['message'] = 'Message';
$string['messagecontactrequestsnotification'] = '{$a} is requesting to be added as a contact.';
$string['messagecontactrequestsnotificationsubject'] = 'Contact request from {$a}';
+$string['messagecontentaudio'] = 'Audio';
+$string['messagecontentimage'] = 'Image';
+$string['messagecontentmultimediageneral'] = 'Other media';
+$string['messagecontentvideo'] = 'Video';
$string['messagedrawerviewcontact'] = 'User details for {$a}';
$string['messagedrawerviewcontacts'] = 'Message contacts';
$string['messagedrawerviewconversation'] = 'Conversation with {$a}';
$string['categorymove'] = 'The category \'{$a->name}\' contains {$a->count} questions (some of which may be hidden questions or random questions that are still in use in a quiz). Please choose another category to move them to.';
$string['categorymoveto'] = 'Save in category';
$string['categorynamecantbeblank'] = 'The category name cannot be blank.';
+$string['categorynamewithcount'] = '{$a->name} ({$a->questioncount})';
+$string['categorynamewithidnumber'] = '{$a->name} [{$a->idnumber}]';
+$string['categorynamewithidnumberandcount'] = '{$a->name} [{$a->idnumber}] ({$a->questioncount})';
$string['clickflag'] = 'Flag question';
$string['clicktoflag'] = 'Flag this question for future reference';
$string['clicktounflag'] = 'Remove flag';
$string['eventquestionsimported'] = 'Questions imported';
$string['eventquestionupdated'] = 'Question updated';
$string['export'] = 'Export';
+$string['exportasxml'] = 'Export as Moodle XML';
$string['exportcategory'] = 'Export category';
$string['exportcategory_help'] = 'This setting determines the category from which the exported questions will be taken.
switch ($schedule) {
case BADGE_MESSAGE_DAILY:
- $nextcron = time() + 60 * 60 * 24;
+ $tomorrow = new DateTime("1 day", core_date::get_server_timezone_object());
+ $nextcron = $tomorrow->getTimestamp();
break;
case BADGE_MESSAGE_WEEKLY:
- $nextcron = time() + 60 * 60 * 24 * 7;
+ $nextweek = new DateTime("1 week", core_date::get_server_timezone_object());
+ $nextcron = $nextweek->getTimestamp();
break;
case BADGE_MESSAGE_MONTHLY:
- $nextcron = time() + 60 * 60 * 24 * 7 * 30;
+ $nextmonth = new DateTime("1 month", core_date::get_server_timezone_object());
+ $nextcron = $nextmonth->getTimestamp();
break;
}
];
}
-/**
- * Called on install or upgrade to create default list of backpacks a user can connect to.
- *
- * @return void
- */
-function badges_install_default_backpacks() {
- global $DB;
-
- $record = new stdClass();
- $record->backpackweburl = BADGE_BACKPACKWEBURL;
- $record->backpackapiurl = BADGE_BACKPACKAPIURL;
- $record->apiversion = OPEN_BADGES_V1;
- $record->sortorder = 0;
- $record->password = '';
-
- $bpid = 0;
- if (!($bp = $DB->get_record('badge_external_backpack', array('backpackapiurl' => $record->backpackapiurl)))) {
- $bpid = $DB->insert_record('badge_external_backpack', $record);
- } else {
- $bpid = $bp->id;
- }
- set_config('badges_site_backpack', $bpid);
-
- // All existing backpacks default to V1.
- $DB->set_field('badge_backpack', 'externalbackpackid', $bpid);
-
- $record = new stdClass();
- $record->backpackapiurl = BADGRIO_BACKPACKAPIURL;
- $record->backpackweburl = BADGRIO_BACKPACKWEBURL;
- $record->apiversion = OPEN_BADGES_V2;
- $record->sortorder = 1;
- $record->password = '';
-
- if (!$DB->record_exists('badge_external_backpack', array('backpackapiurl' => $record->backpackapiurl))) {
- $DB->insert_record('badge_external_backpack', $record);
- }
-
-}
-
/**
* Get the default issuer for a badge from this site.
*
use Behat\Mink\Element\Element;
use Behat\Mink\Session;
+require_once(__DIR__ . '/classes/component_named_selector.php');
+require_once(__DIR__ . '/classes/component_named_replacement.php');
+
/**
* Steps definitions base class.
*
$selector = 'xpath';
}
- // Convert to named_partial where the selector type is not named_partial, named_exact, xpath, or css.
+ // Convert to a named selector where the selector type is not a known selector.
$converttonamed = !$this->getSession()->getSelectorsHandler()->isSelectorRegistered($selector);
$converttonamed = $converttonamed && 'xpath' !== $selector;
if ($converttonamed) {
+ if (behat_partial_named_selector::is_deprecated_selector($selector)) {
+ if ($replacement = behat_partial_named_selector::get_deprecated_replacement($selector)) {
+ error_log("The '{$selector}' selector has been replaced with {$replacement}");
+ $selector = $replacement;
+ }
+ } else if (behat_exact_named_selector::is_deprecated_selector($selector)) {
+ if ($replacement = behat_exact_named_selector::get_deprecated_replacement($selector)) {
+ error_log("The '{$selector}' selector has been replaced with {$replacement}");
+ $selector = $replacement;
+ }
+ }
+
$allowedpartialselectors = behat_partial_named_selector::get_allowed_selectors();
$allowedexactselectors = behat_exact_named_selector::get_allowed_selectors();
if (isset($allowedpartialselectors[$selector])) {
$locator = behat_selectors::normalise_named_selector($allowedexactselectors[$selector], $locator);
$selector = 'named_exact';
} else {
- throw new ExpectationException("The '{$selector}' selector type is not registered.", $this);
+ throw new ExpectationException("The '{$selector}' selector type is not registered.", $this->getSession()->getDriver());
}
}
}
}
+ /**
+ * Convert page names to URLs for steps like 'When I am on the "[page name]" page'.
+ *
+ * You should override this as appropriate for your plugin. The method
+ * {@link behat_navigation::resolve_core_page_url()} is a good example.
+ *
+ * Your overridden method should document the recognised page types with
+ * a table like this:
+ *
+ * Recognised page names are:
+ * | Page | Description |
+ *
+ * @param string $page name of the page, with the component name removed e.g. 'Admin notification'.
+ * @return moodle_url the corresponding URL.
+ * @throws Exception with a meaningful error message if the specified page cannot be found.
+ */
+ protected function resolve_page_url(string $page): moodle_url {
+ throw new Exception('Component "' . get_class($this) .
+ '" does not support the generic \'When I am on the "' . $page .
+ '" page\' navigation step.');
+ }
+
+ /**
+ * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
+ *
+ * A typical example might be:
+ * When I am on the "Test quiz" "mod_quiz > Responses report" page
+ * which would cause this method in behat_mod_quiz to be called with
+ * arguments 'Responses report', 'Test quiz'.
+ *
+ * You should override this as appropriate for your plugin. The method
+ * {@link behat_navigation::resolve_core_page_instance_url()} is a good example.
+ *
+ * Your overridden method should document the recognised page types with
+ * a table like this:
+ *
+ * Recognised page names are:
+ * | Type | identifier meaning | Description |
+ *
+ * @param string $type identifies which type of page this is, e.g. 'Attempt review'.
+ * @param string $identifier identifies the particular page, e.g. 'Test quiz > student > Attempt 1'.
+ * @return moodle_url the corresponding URL.
+ * @throws Exception with a meaningful error message if the specified page cannot be found.
+ */
+ protected function resolve_page_instance_url(string $type, string $identifier): moodle_url {
+ throw new Exception('Component "' . get_class($this) .
+ '" does not support the generic \'When I am on the "' . $identifier .
+ '" "' . $type . '" page\' navigation step.');
+ }
+
/**
* Gets the required timeout in seconds.
*
public static function get_extended_timeout() : int {
return self::get_real_timeout(10);
}
+
+ /**
+ * Return a list of the exact named selectors for the component.
+ *
+ * Named selectors are what make Behat steps like
+ * Then I should see "Useful text" in the "General" "fieldset"
+ * work. Here, "fieldset" is the named selector, and "General" is the locator.
+ *
+ * If you override this method in your plugin (e.g. mod_mymod), to define
+ * new selectors specific to your plugin. For example, if you returned
+ * new behat_component_named_selector('Thingy',
+ * [".//some/xpath//img[contains(@alt, %locator%)]/.."])
+ * then
+ * Then I should see "Useful text" in the "Whatever" "mod_mymod > Thingy"
+ * would work.
+ *
+ * This method should return a list of {@link behat_component_named_selector} and
+ * the docs on that class explain how it works.
+ *
+ * @return behat_component_named_selector[]
+ */
+ public static function get_exact_named_selectors(): array {
+ return [];
+ }
+
+ /**
+ * Return a list of the partial named selectors for the component.
+ *
+ * Like the exact named selectors above, but the locator only
+ * needs to match part of the text. For example, the standard
+ * "button" is a partial selector, so:
+ * When I click "Save" "button"
+ * will activate "Save changes".
+ *
+ * @return behat_component_named_selector[]
+ */
+ public static function get_partial_named_selectors(): array {
+ return [];
+ }
+
+ /**
+ * Return a list of the Mink named replacements for the component.
+ *
+ * Named replacements allow you to define parts of an xpath that can be reused multiple times, or in multiple
+ * xpaths.
+ *
+ * This method should return a list of {@link behat_component_named_replacement} and the docs on that class explain
+ * how it works.
+ *
+ * @return behat_component_named_replacement[]
+ */
+ public static function get_named_replacements(): array {
+ return [];
+ }
}
* @return behat_base
*/
public static function get($classname) {
- $contexts = self::$environment->getContexts();
-
- foreach ($contexts as $context) {
- if (is_a($context, $classname)) {
- return $context;
- }
+ if (self::$environment->hasContextClass($classname)) {
+ return self::$environment->getContext($classname);
}
$suitename = self::$environment->getSuite()->getName();
return self::$environment->getContext($classname);
}
+ /**
+ * Return whether there is a context of the specified classname.
+ *
+ * @param string $classname
+ * @return bool
+ */
+ public static function has_context(string $classname): bool {
+ return self::$environment->hasContextClass($classname);
+ }
+
/**
* Translates string to XPath literal.
*
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+require_once(__DIR__ . '/named_selector.php');
require_once(__DIR__ . '/exact_named_selector.php');
require_once(__DIR__ . '/partial_named_selector.php');
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A class for recording the definition of Mink replacements.
+ *
+ * @package core
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * A class for recording the definition of Mink replacements for use in Mink selectors.
+ *
+ * These are comprised of a source string, and a replacement.
+ *
+ * During use the source string is converted from the string to be in the format:
+ *
+ * %[component]/[string]%
+ *
+ * For example:
+ *
+ * %mod_forum/title%
+ *
+ * Mink replacements are used in xpath translation to translate regularly used items such as title.
+ * Here is an example from the upstream Mink project:
+ *
+ * '%tagTextMatch%' => 'contains(normalize-space(string(.)), %locator%)'
+ *
+ * And can be used in an xpath:
+ *
+ * .//label[%tagTextMatch%]
+ *
+ * This would be expanded to:
+ *
+ * .//label[contains(normalize-space(string(.)), %locator%)]
+ *
+ * Replacements can also be used in other replacements, as long as that replacement is defined later.
+ *
+ * '%linkMatch%' => '(%idMatch% or %tagTextMatch% or %titleMatch% or %relMatch%)'
+ *
+ * @package core
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_component_named_replacement {
+ /** @var string */
+ protected $from;
+
+ /** @var string */
+ protected $to;
+
+ /**
+ * Create the replacement.
+ *
+ * @param string $from this is the old selector that should no longer be used.
+ * For example 'group_message'.
+ * @param string $to this is the new equivalent that should be used instead.
+ * For example 'core_message > Message'.
+ */
+ public function __construct(string $from, string $to) {
+ $this->from = $from;
+ $this->to = $to;
+ }
+
+ /**
+ * Get the 'from' part of the replacement, formatted for the component.
+ *
+ * @param string $component
+ * @return string
+ */
+ public function get_from(string $component): string {
+ return "%{$component}/{$this->from}%";
+ }
+
+ /**
+ * Get the 'to' part of the replacement.
+ *
+ * @return string Target xpath
+ */
+ public function get_to(): string {
+ return $this->to;
+ }
+}
--- /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 representing a named selector that can be used in Behat tests.
+ *
+ * @package core
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Class representing a named selector that can be used in Behat tests.
+ *
+ * Named selectors are what make Behat steps like
+ * Then I should see "Useful text" in the "General" "fieldset"
+ * Here, "fieldset" is the named selector, and "General" is the locator.
+ *
+ * Selectors can either be exact, in which case the locator needs to
+ * match exactly, or can be partial, for example the way
+ * When I click "Save" "button"
+ * will trigger a "Save changes" button.
+ *
+ * Instances of this class get returned by the get_exact_named_selectors()
+ * and get_partial_named_selectors() methods in classes like behat_mod_mymod.
+ * The code that makes the magic work is in the trait behat_named_selector
+ * used by both behat_exact_named_selector and behat_partial_named_selector.
+ *
+ * @package core
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_component_named_selector {
+ /** @var string */
+ protected $alias;
+
+ /** @var array List of xpaths */
+ protected $xpaths;
+
+ /** @var string */
+ protected $istextselector;
+
+ /**
+ * Create the selector definition.
+ *
+ * As an example, if you define
+ * new behat_component_named_selector('Message',
+ * [".//*[@data-conversation-id]//img[contains(@alt, %locator%)]/.."])
+ * in get_partial_named_selectors in behat_message in
+ * message/tests/behat/behat_message.php, then steps like
+ * When "Group 1" "core_message > Message" should exist
+ * will work.
+ *
+ * Text selectors are things that contain other things (e.g. some particular text), e.g.
+ * Then I can see "Some text" in the "Whatever" "text_selector"
+ * whereas non-text selectors are atomic things, like
+ * When I click the "Whatever" "widget".
+ *
+ * @param string $alias The 'friendly' name of the thing. This will be prefixed with the component name.
+ * For example, if the mod_mymod plugin, says 'Thingy', then "mod_mymod > Thingy" becomes a selector.
+ * @param array $xpaths A list of xpaths one or more XPaths that the selector gets transformed into.
+ * @param bool $istextselector Whether this selector can also be used as a text selector.
+ */
+ public function __construct(string $alias, array $xpaths, bool $istextselector = true) {
+ $this->alias = $alias;
+ $this->xpaths = $xpaths;
+ $this->istextselector = $istextselector;
+ }
+
+ /**
+ * Whether this is a text selector.
+ *
+ * @return bool
+ */
+ public function is_text_selector(): bool {
+ return $this->istextselector;
+ }
+
+ /**
+ * Get the name of the selector.
+ * This is a back-end feature and contains a namespaced md5 of the human-readable name.
+ *
+ * @param string $component
+ * @return string
+ */
+ public function get_name(string $component): string {
+ return implode('_', [$component, md5($this->alias)]);
+ }
+
+ /**
+ * Get the alias of the selector.
+ * This is the human-readable name that you would typically interact with.
+ *
+ * @param string $component
+ * @return string
+ */
+ public function get_alias(string $component): string {
+ return implode(" > ", [$component, $this->alias]);;
+ }
+
+ /**
+ * Get the list of combined xpaths.
+ *
+ * @return string The list of xpaths combined with the xpath | (OR) operator
+ */
+ public function get_combined_xpath(): string {
+ return implode(' | ', $this->xpaths);
+ }
+}
*/
class behat_exact_named_selector extends \Behat\Mink\Selector\ExactNamedSelector {
+ // Use the named selector trait.
+ use behat_named_selector;
+
/**
* Creates selector instance.
*/
'text_exact' => 'text',
);
+ /** @var List of deprecated selectors */
+ protected static $deprecatedselectors = [];
+
/**
* Allowed selectors getter.
*
--- /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/>.
+
+/**
+ * Moodle-specific common functions for named selectors.
+ *
+ * @package core
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Common functions for named selectors.
+ *
+ * This has to be a trait, because we need this in both the classes
+ * behat_exact_named_selector and behat_partial_named_selector, and
+ * those classes have to be subclasses of \Behat\Mink\Selector\ExactNamedSelector
+ * and \Behat\Mink\Selector\PartialNamedSelector. This trait is a way achieve
+ * that without duplciated code.
+ *
+ * @package core
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+trait behat_named_selector {
+
+ /**
+ * Registers new XPath selector with specified name.
+ *
+ * @param string $component
+ * @param behat_component_named_selector $selector
+ */
+ public function register_component_selector(string $component, behat_component_named_selector $selector) {
+ $alias = $selector->get_alias($component);
+ $name = $selector->get_name($component);
+ static::$allowedselectors[$alias] = $name;
+
+ if ($selector->is_text_selector()) {
+ static::$allowedtextselectors[$alias] = $name;
+ }
+
+ // We must use Reflection here. The replacements property is private and cannot be accessed otherwise.
+ // This is due to an API limitation in Mink.
+ $rc = new \ReflectionClass(\Behat\Mink\Selector\NamedSelector::class);
+ $r = $rc->getProperty('replacements');
+ $r->setAccessible(true);
+ $replacements = $r->getValue($this);
+
+ $selectorxpath = strtr($selector->get_combined_xpath(), $replacements);
+
+ parent::registerNamedXpath($name, $selectorxpath);
+ }
+
+ /**
+ * Registers new XPath selector with specified name.
+ *
+ * @param string $component
+ * @param behat_component_named_replacement $replacement
+ */
+ public function register_replacement(string $component, behat_component_named_replacement $replacement) {
+ // We must use Reflection here. The replacements property is private and cannot be accessed otherwise.
+ // This is due to an API limitation in Mink.
+ $rc = new \ReflectionClass(\Behat\Mink\Selector\NamedSelector::class);
+ $r = $rc->getProperty('replacements');
+ $r->setAccessible(true);
+ $existing = $r->getValue($this);
+
+ $from = $replacement->get_from($component);
+
+ if (isset($existing[$from])) {
+ throw new \coding_exception("A named replacement already exists in the partial named selector for '{$from}'. " .
+ "Replacement names must be unique, and should be namespaced to the component");
+ }
+
+ $translatedto = strtr($replacement->get_to(), $existing);
+ $this->registerReplacement($from, $translatedto);
+ }
+
+ /**
+ * Check whether the specified selector has been deprecated and marked for replacement.
+ *
+ * @param string $selector
+ * @return bool
+ */
+ public static function is_deprecated_selector(string $selector): bool {
+ return array_key_exists($selector, static::$deprecatedselectors);
+ }
+
+ /**
+ * Fetch the replacement name of a deprecated selector.
+ *
+ * @param string $selector
+ * @return bool
+ */
+ public static function get_deprecated_replacement(string $selector): ?string {
+ return static::$deprecatedselectors[$selector];
+ }
+}
*/
class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSelector {
+ // Use the named selector trait.
+ use behat_named_selector;
+
/**
* Creates selector instance.
*/
],
];
+ /** @var List of deprecated selectors */
+ protected static $deprecatedselectors = [
+ 'group_message' => 'core_message > Message',
+ 'group_message_member' => 'core_message > Message member',
+ 'group_message_tab' => 'core_message > Message tab',
+ 'group_message_list_area' => 'core_message > Message list area',
+ 'group_message_message_content' => 'core_message > Message content',
+ ];
+
/**
* Allowed selectors getter.
*
return behat_command::get_parent_behat_dir() . '/test_environment_enabled.txt';
}
+ /**
+ * Removes config settings that were added to the main $CFG config within the Behat CLI
+ * run.
+ *
+ * Database storage is already handled by reset_database and existing config values will
+ * be reset automatically by initialise_cfg(), so we only need to remove added ones.
+ */
+ public static function remove_added_config() {
+ global $CFG;
+ if (!empty($CFG->behat_cli_added_config)) {
+ foreach ($CFG->behat_cli_added_config as $key => $value) {
+ unset($CFG->{$key});
+ }
+ unset($CFG->behat_cli_added_config);
+ }
+ }
+
/**
* Reset contents of all database tables to initial values, reset caches, etc.
*/
// Initialise $CFG with default values. This is needed for behat cli process, so we don't have modified
// $CFG values from the old run. @see set_config.
+ self::remove_added_config();
initialise_cfg();
}
* <ul>
* <li><b>id </b> string - The element ID.</li>
* <li><b>name </b> string - The element name.</li>
- * <li><b>classes </b> string - CSS classes that you want to add for your checkbox.</li>
+ * <li><b>classes </b> string - CSS classes that you want to add for your checkbox or toggle controls.
+ * For button type master toggle controls, this could be any Bootstrap 4 btn classes
+ * that you might want to add. Defaults to "btn-secondary".</li>
* <li><b>value </b> string|int - The element's value.</li>
* <li><b>checked </b> boolean - Whether to render this initially as checked.</li>
* <li><b>label </b> string - The label for the checkbox element.</li>
'core:i/mean' => 'fa-calculator',
'core:i/menu' => 'fa-ellipsis-v',
'core:i/menubars' => 'fa-bars',
+ 'core:i/messagecontentaudio' => 'fa-headphones',
+ 'core:i/messagecontentimage' => 'fa-image',
+ 'core:i/messagecontentvideo' => 'fa-film',
+ 'core:i/messagecontentmultimediageneral' => 'fa-file-video-o',
'core:i/mnethost' => 'fa-external-link',
'core:i/moodle_host' => 'fa-graduation-cap',
'core:i/moremenu' => 'fa-ellipsis-h',
$extension = strtolower($extension);
$supportedvideo = array('m4v', 'webm', 'ogv', 'mp4', 'mov');
- $supportedaudio = array('ogg', 'oga', 'aac', 'm4a', 'mp3', 'wav');
- // TODO MDL-56549 Flac will be supported in Firefox 51 in January 2017.
+ $supportedaudio = array('ogg', 'oga', 'aac', 'm4a', 'mp3', 'wav', 'flac');
// Basic extension support.
if (!in_array($extension, $supportedvideo) && !in_array($extension, $supportedaudio)) {
if ($isogg && (self::is_ie() || self::is_edge() || self::is_safari() || self::is_safari_ios())) {
return false;
}
+ // FLAC is not supported in IE and Edge (below 16.0).
+ if ($extension === 'flac' &&
+ (self::is_ie() || (self::is_edge() && !self::check_edge_version('16.0')))) {
+ return false;
+ }
// Wave is not supported in IE.
if ($extension === 'wav' && self::is_ie()) {
return false;
'riskbitmask' => RISK_XSS,
'captype' => 'write',
- 'contextlevel' => CONTEXT_SYSTEM,
+ 'contextlevel' => CONTEXT_COURSECAT,
'archetypes' => array(
'manager' => CAP_ALLOW
)
'moodle/course:request' => array(
'captype' => 'write',
- 'contextlevel' => CONTEXT_SYSTEM,
- 'archetypes' => array(
- 'user' => CAP_ALLOW,
- )
+ 'contextlevel' => CONTEXT_COURSECAT,
),
'moodle/course:delete' => array(
make_default_scale();
make_competence_scale();
- require_once($CFG->libdir . '/badgeslib.php');
+ require_once($CFG->dirroot . '/badges/upgradelib.php'); // Core install and upgrade related functions only for badges.
badges_install_default_backpacks();
}
if ($oldversion < 2018092800.02) {
// Delete any contacts that are not mutual (meaning they both haven't added each other).
- $sql = "SELECT c1.id
- FROM {message_contacts} c1
- LEFT JOIN {message_contacts} c2
- ON c1.userid = c2.contactid
- AND c1.contactid = c2.userid
- WHERE c2.id IS NULL";
- if ($contacts = $DB->get_records_sql($sql)) {
- list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contacts));
- $DB->delete_records_select('message_contacts', "id $insql", $inparams);
- }
+ do {
+ $sql = "SELECT c1.id
+ FROM {message_contacts} c1
+ LEFT JOIN {message_contacts} c2
+ ON c1.userid = c2.contactid
+ AND c1.contactid = c2.userid
+ WHERE c2.id IS NULL";
+ if ($contacts = $DB->get_records_sql($sql, null, 0, 1000)) {
+ list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contacts));
+ $DB->delete_records_select('message_contacts', "id $insql", $inparams);
+ }
+ } while ($contacts);
upgrade_main_savepoint(true, 2018092800.02);
}
}
// Add default backpacks.
- require_once($CFG->libdir.'/badgeslib.php'); // Core Upgrade-related functions for badges only.
+ require_once($CFG->dirroot . '/badges/upgradelib.php'); // Core install and upgrade related functions only for badges.
badges_install_default_backpacks();
// Main savepoint reached.
upgrade_main_savepoint(true, 2019100900.00);
}
+ if ($oldversion < 2019101600.01) {
+
+ // Change the setting $CFG->requestcategoryselection into $CFG->lockrequestcategory with opposite value.
+ set_config('lockrequestcategory', !$CFG->requestcategoryselection);
+
+ upgrade_main_savepoint(true, 2019101600.01);
+ }
+
return true;
}
}
}
- if ($accelerate and !empty($CFG->xsendfile)) {
- if (empty($CFG->disablebyteserving) and $mimetype !== 'text/plain') {
- header('Accept-Ranges: bytes');
- } else {
- header('Accept-Ranges: none');
- }
+ if ($accelerate and empty($CFG->disablebyteserving) and $mimetype !== 'text/plain') {
+ header('Accept-Ranges: bytes');
+ } else {
+ header('Accept-Ranges: none');
+ }
+ if ($accelerate) {
if (is_object($file)) {
$fs = get_file_storage();
- if ($fs->xsendfile($file->get_contenthash())) {
- return;
+ if ($fs->supports_xsendfile()) {
+ if ($fs->xsendfile($file->get_contenthash())) {
+ return;
+ }
}
-
} else {
- require_once("$CFG->libdir/xsendfilelib.php");
- if (xsendfile($file)) {
- return;
+ if (!empty($CFG->xsendfile)) {
+ require_once("$CFG->libdir/xsendfilelib.php");
+ if (xsendfile($file)) {
+ return;
+ }
}
}
}
header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
if ($accelerate and empty($CFG->disablebyteserving) and $mimetype !== 'text/plain') {
- header('Accept-Ranges: bytes');
if (!empty($_SERVER['HTTP_RANGE']) and strpos($_SERVER['HTTP_RANGE'],'bytes=') !== FALSE) {
// byteserving stuff - for acrobat reader and download accelerators
byteserving_send_file($handle, $mimetype, $ranges, $filesize);
}
}
- } else {
- // Do not byteserve
- header('Accept-Ranges: none');
}
header('Content-Length: '.$filesize);
return $this->filesystem->xsendfile($contenthash);
}
+ /**
+ * Returns true if filesystem is configured to support xsendfile.
+ *
+ * @return bool
+ */
+ public function supports_xsendfile() {
+ return $this->filesystem->supports_xsendfile();
+ }
+
/**
* Content exists
*
return xsendfile($this->get_remote_path_from_hash($contenthash));
}
+ /**
+ * Returns true if filesystem is configured to support xsendfile.
+ *
+ * @return bool
+ */
+ public function supports_xsendfile() {
+ global $CFG;
+ return !empty($CFG->xsendfile);
+ }
+
/**
* Validate that the content hash matches the content hash of the file on disk.
*
public function compare_to_string($content) {
return $this->get_contenthash() === file_storage::hash_from_string($content);
}
+
+ /**
+ * Generate a rotated image for this stored_file based on exif information.
+ *
+ * @return array|false False when a problem occurs, else the image data and image size.
+ * @since Moodle 3.8
+ */
+ public function rotate_image() {
+ $content = $this->get_content();
+ $mimetype = $this->get_mimetype();
+
+ if ($mimetype === "image/jpeg" && function_exists("exif_read_data")) {
+ $exif = @exif_read_data("data://image/jpeg;base64," . base64_encode($content));
+ if (isset($exif['ExifImageWidth']) && isset($exif['ExifImageLength']) && isset($exif['Orientation'])) {
+ $rotation = [
+ 3 => -180,
+ 6 => -90,
+ 8 => -270,
+ ];
+ $orientation = $exif['Orientation'];
+ if ($orientation !== 1) {
+ $source = @imagecreatefromstring($content);
+ $data = @imagerotate($source, $rotation[$orientation], 0);
+ if (!empty($data)) {
+ if ($orientation == 1 || $orientation == 3) {
+ $size = [
+ 'width' => $exif["ExifImageWidth"],
+ 'height' => $exif["ExifImageLength"],
+ ];
+ } else {
+ $size = [
+ 'height' => $exif["ExifImageWidth"],
+ 'width' => $exif["ExifImageLength"],
+ ];
+ }
+ imagedestroy($source);
+ return [$data, $size];
+ }
+ }
+ }
+ }
+ return [false, false];
+ }
}
}
}
}
-
- // The YUI2 calendar only supports the gregorian calendar type.
- if ($calendartype->get_name() === 'gregorian') {
- form_init_date_js();
- }
}
/**
* @param string $error An error message associated with a group
*/
function accept(&$renderer, $required = false, $error = null) {
+ form_init_date_js();
$renderer->renderElement($this, $required, $error);
}
+ /**
+ * Export for template
+ *
+ * @param renderer_base $output
+ * @return array|stdClass
+ */
+ public function export_for_template(renderer_base $output) {
+ form_init_date_js();
+ return parent::export_for_template($output);
+ }
+
/**
* Output a timestamp. Give it the name of the group.
*
}
}
}
-
- // The YUI2 calendar only supports the gregorian calendar type.
- if ($calendartype->get_name() === 'gregorian') {
- form_init_date_js();
- }
}
/**
* @param string $error An error message associated with a group
*/
function accept(&$renderer, $required = false, $error = null) {
+ form_init_date_js();
$renderer->renderElement($this, $required, $error);
}
+ /**
+ * Export for template
+ *
+ * @param renderer_base $output
+ * @return array|stdClass
+ */
+ public function export_for_template(renderer_base $output) {
+ form_init_date_js();
+ return parent::export_for_template($output);
+ }
+
/**
* Output a timestamp. Give it the name of the group.
*
global $PAGE;
static $done = false;
if (!$done) {
+ $done = true;
$calendar = \core_calendar\type_factory::get_calendar_instance();
+ if ($calendar->get_name() !== 'gregorian') {
+ // The YUI2 calendar only supports the gregorian calendar type.
+ return;
+ }
$module = 'moodle-form-dateselector';
$function = 'M.form.dateselector.init_date_selectors';
$defaulttimezone = date_default_timezone_get();
'december' => date_format_string(strtotime("December 1"), '%B', $defaulttimezone)
));
$PAGE->requires->yui_module($module, $function, $config);
- $done = true;
}
}
$config->value = $value;
$DB->insert_record('config', $config, false);
}
+ // When setting config during a Behat test (in the CLI script, not in the web browser
+ // requests), remember which ones are set so that we can clear them later.
+ if (defined('BEHAT_TEST')) {
+ if (!property_exists($CFG, 'behat_cli_added_config')) {
+ $CFG->behat_cli_added_config = [];
+ }
+ $CFG->behat_cli_added_config[$name] = true;
+ }
}
if ($name === 'siteidentifier') {
cache_helper::update_site_identifier($value);
/**
* An icon to use for the toggling the secondary menu (dropdown).
- * @var actionicon
+ * @var pix_icon
*/
public $actionicon;
/**
* Any text to use for the toggling the secondary menu (dropdown).
- * @var menutrigger
+ * @var string
*/
public $menutrigger = '';
/**
* Any extra classes for toggling to the secondary menu.
- * @var triggerextraclasses
+ * @var string
*/
public $triggerextraclasses = '';
/**
* Place the action menu before all other actions.
- * @var prioritise
+ * @var bool
*/
public $prioritise = false;
/**
* Constructs the action menu with the given items.
*
- * @param array $actions An array of actions.
+ * @param array $actions An array of actions (action_menu_link|pix_icon|string).
*/
public function __construct(array $actions = array()) {
static $initialised = 0;
* Sets the label for the menu trigger.
*
* @param string $label The text
- * @return null
*/
public function set_action_label($label) {
$this->actionlabel = $label;
*
* @param string $trigger The text
* @param string $extraclasses Extra classes to style the secondary menu toggle.
- * @return null
*/
public function set_menu_trigger($trigger, $extraclasses = '') {
$this->menutrigger = $trigger;
* @param int $limit limit the number of tags to display, if size of $tags is more than this limit the "more" link
* will be appended to the end, JS will toggle the rest of the tags
* @param context $pagecontext specify if needed to overwrite the current page context for the view tag link
+ * @param bool $accesshidelabel if true, the label should have class="accesshide" added.
* @return string
*/
- public function tag_list($tags, $label = null, $classes = '', $limit = 10, $pagecontext = null) {
- $list = new \core_tag\output\taglist($tags, $label, $classes, $limit, $pagecontext);
+ public function tag_list($tags, $label = null, $classes = '', $limit = 10,
+ $pagecontext = null, $accesshidelabel = false) {
+ $list = new \core_tag\output\taglist($tags, $label, $classes, $limit, $pagecontext, $accesshidelabel);
return $this->render_from_template('core_tag/taglist', $list->export_for_template($this));
}
if ($category->contextid == $contextid) {
$cid = $category->id;
if ($currentcat != $cid || $currentcat == 0) {
- $countstring = !empty($category->questioncount) ?
- " ($category->questioncount)" : '';
- $categoriesarray[$contextstring][$cid] =
- format_string($category->indentedname, true,
- array('context' => $context)) . $countstring;
+ $a = new stdClass;
+ $a->name = format_string($category->indentedname, true,
+ array('context' => $context));
+ if ($category->idnumber !== null && $category->idnumber !== '') {
+ $a->idnumber = s($category->idnumber);
+ }
+ if (!empty($category->questioncount)) {
+ $a->questioncount = $category->questioncount;
+ }
+ if (isset($a->idnumber) && isset($a->questioncount)) {
+ $formattedname = get_string('categorynamewithidnumberandcount', 'question', $a);
+ } else if (isset($a->idnumber)) {
+ $formattedname = get_string('categorynamewithidnumber', 'question', $a);
+ } else if (isset($a->questioncount)) {
+ $formattedname = get_string('categorynamewithcount', 'question', $a);
+ } else {
+ $formattedname = $a->name;
+ }
+ $categoriesarray[$contextstring][$cid] = $formattedname;
}
}
}
}
/**
- * @return array all parent contexts
+ * @return context[] all parent contexts
*/
public function all() {
return $this->allcontexts;
}
/**
- * @return object lowest context which must be either the module or course context
+ * @return context lowest context which must be either the module or course context
*/
public function lowest() {
return $this->allcontexts[0];
/**
* @param string $cap capability
- * @return array parent contexts having capability, zero based index
+ * @return context[] parent contexts having capability, zero based index
*/
public function having_cap($cap) {
$contextswithcap = array();
/**
* @param array $caps capabilities
- * @return array parent contexts having at least one of $caps, zero based index
+ * @return context[] parent contexts having at least one of $caps, zero based index
*/
public function having_one_cap($caps) {
$contextswithacap = array();
/**
* @param string $tabname edit tab name
- * @return array parent contexts having at least one of $caps, zero based index
+ * @return context[] parent contexts having at least one of $caps, zero based index
*/
public function having_one_edit_tab_cap($tabname) {
return $this->having_one_cap(self::$caps[$tabname]);
}
/**
- * @return those contexts where a user can add a question and then use it.
+ * @return context[] those contexts where a user can add a question and then use it.
*/
public function having_add_and_use() {
$contextswithcap = array();
/**
* Throw error if at least one parent context hasn't got one of the caps $caps
*
- * @param array $cap capabilities
+ * @param array $caps capabilities
*/
public function require_one_cap($caps) {
if (!$this->have_one_cap($caps)) {
* @return string contents of cell in column 'fullname', for this row.
*/
function col_fullname($row) {
- global $COURSE;
+ global $PAGE, $COURSE;
- $name = fullname($row);
+ $name = fullname($row, has_capability('moodle/site:viewfullnames', $PAGE->context));
if ($this->download) {
return $name;
}
"togglegroup": "toggle-group",
"label": "Select everything!",
"checked": true,
- "classes": "p-1",
+ "classes": "btn-primary btn-lg",
"selectall": "Select all",
"deselectall": "Deselect all"
}
}}
-<button type="button" id="{{id}}" name="{{name}}" class="btn btn-secondary {{classes}}"
+<button type="button" id="{{id}}" name="{{name}}" class="btn {{^classes}}btn-secondary{{/classes}}{{#classes}}{{.}}{{/classes}}"
data-action="toggle"
data-toggle="master"
data-togglegroup="{{togglegroup}}"
"checked": true,
"classes": "p-1",
"selectall": "Select all",
- "deselectall": "Deselect all"
+ "deselectall": "Deselect all",
+ "labelclasses": "badge badge-info"
}
}}
<input id="{{id}}" name="{{name}}" type="checkbox" {{#classes}}class="{{.}}"{{/classes}} value="{{value}}"
"togglegroup": "toggle-group",
"label": "Select me!",
"checked": true,
- "classes": "p-1"
+ "classes": "p-1",
+ "labelclasses": "badge badge-info"
}
}}
<input id="{{id}}" name="{{name}}" type="checkbox" {{#classes}}class="{{.}}"{{/classes}} value="{{value}}"
* @Then :preelement :preselectortype should appear before :postelement :postselectortype in the :containerelement :containerselectortype
* @throws ExpectationException
* @param string $preelement The locator of the preceding element
- * @param string $preselectortype The locator of the preceding element
+ * @param string $preselectortype The selector type of the preceding element
* @param string $postelement The locator of the latest element
* @param string $postselectortype The selector type of the latest element
* @param string $containerelement
?string $containerelement = null,
?string $containerselectortype = null
) {
- $msg = "'{$preelement}' '{$preselectortype}' does not appear after '{$postelement}' '{$postselectortype}'";
+ $msg = "'{$preelement}' '{$preselectortype}' does not appear before '{$postelement}' '{$postselectortype}'";
$this->check_element_order(
$containerelement,
$containerselectortype,
* @param string $postelement The locator of the latest element
* @param string $postselectortype The selector type of the latest element
* @param string $preelement The locator of the preceding element
- * @param string $preselectortype The locator of the preceding element
+ * @param string $preselectortype The selector type of the preceding element
* @param string $containerelement
* @param string $containerselectortype
*/
// Register behat selectors for theme, if suite is changed. We do it for every suite change.
if ($suitename !== self::$runningsuite) {
+ self::$runningsuite = $suitename;
behat_context_helper::set_environment($scope->getEnvironment());
// We need the Mink session to do it and we do it only before the first scenario.
$this->getSession()->getSelectorsHandler()->registerSelector('named_partial', new $namedpartialclass());
$this->getSession()->getSelectorsHandler()->registerSelector('named_exact', new $namedexactclass());
+
+ // Register component named selectors.
+ foreach (\core_component::get_component_names() as $component) {
+ $this->register_component_selectors_for_component($component);
+ }
+
}
// Reset mink session between the scenarios.
// Set the theme if not default.
if ($suitename !== "default") {
set_config('theme', $suitename);
- self::$runningsuite = $suitename;
}
// Reset the scenariorunning variable to ensure that Step 0 occurs.
* Yes, this is in a strange location and should be in the BeforeScenario hook, but failures in the test setUp lead
* to the test being incorrectly marked as skipped with no way to force the test to be failed.
*
- * @param BeforeStepScope $scope
+ * @param BeforeStepScope $scope
* @BeforeStep
*/
public function before_step(BeforeStepScope $scope) {
new ExpectationException($message, $session)
);
- self::$initprocessesfinished = true;
}
$this->scenariorunning = true;
}
protected static function is_first_scenario() {
return !(self::$initprocessesfinished);
}
+
+ /**
+ * Register a set of component selectors.
+ *
+ * @param string $component
+ */
+ public function register_component_selectors_for_component(string $component): void {
+ $componentclassname = "behat_{$component}";
+
+ if (!behat_context_helper::has_context($componentclassname)) {
+ if ("core_" === substr($component, 0, 5)) {
+ $componentclassname = "behat_" . substr($component, 5);
+ if (!behat_context_helper::has_context($componentclassname)) {
+ return;
+ }
+ } else {
+ return;
+ }
+ }
+
+ $context = behat_context_helper::get($componentclassname);
+ $namedpartial = $this->getSession()->getSelectorsHandler()->getSelector('named_partial');
+ $namedexact = $this->getSession()->getSelectorsHandler()->getSelector('named_exact');
+
+ // Replacements must come before selectors as they are used in the selectors.
+ foreach ($context->get_named_replacements() as $replacement) {
+ $namedpartial->register_replacement($component, $replacement);
+ $namedexact->register_replacement($component, $replacement);
+ }
+
+ foreach ($context->get_partial_named_selectors() as $selector) {
+ $namedpartial->register_component_selector($component, $selector);
+ }
+
+ foreach ($context->get_exact_named_selectors() as $selector) {
+ $namedexact->register_component_selector($component, $selector);
+ }
+
+ }
+
+ /**
+ * Mark the first step as having been completed.
+ *
+ * This must be the last BeforeStep hook in the setup.
+ *
+ * @param BeforeStepScope $scope
+ * @BeforeStep
+ */
+ public function first_step_setup_complete(BeforeStepScope $scope) {
+ self::$initprocessesfinished = true;
+ }
+
}
/**
$USER = $globuser;
}
+ /**
+ * Open a given page, belonging to a plugin or core component.
+ *
+ * The page-type are interpreted by each plugin to work out the
+ * corresponding URL. See the resolve_url method in each class like
+ * behat_mod_forum. That method should document which page types are
+ * recognised, and how the name identifies them.
+ *
+ * For pages belonging to core, the 'core > ' bit is omitted.
+ *
+ * @When I am on the :page page
+ * @param string $page the component and page name.
+ * E.g. 'Admin notifications' or 'core_user > Preferences'.
+ * @throws Exception if the specified page cannot be determined.
+ */
+ public function i_am_on_page(string $page) {
+ $this->getSession()->visit($this->locate_path(
+ $this->resolve_page_helper($page)->out_as_local_url()));
+ }
+
+ /**
+ * Open a given page logged in as a given user.
+ *
+ * This is like the combination
+ * When I log in as "..."
+ * And I am on the "..." page
+ * but with the advantage that you go straight to the desired page, without
+ * having to wait for the Dashboard to load.
+ *
+ * @When I am on the :page page logged in as :username
+ * @param string $page the type of page. E.g. 'Admin notifications' or 'core_user > Preferences'.
+ * @param string $username the name of the user to log in as. E.g. 'admin'.
+ * @throws Exception if the specified page cannot be determined.
+ */
+ public function i_am_on_page_logged_in_as(string $page, string $username) {
+ self::execute('behat_auth::i_log_in_as', [$username, $this->resolve_page_helper($page)]);
+ }
+
+ /**
+ * Helper used by i_am_on_page() and i_am_on_page_logged_in_as().
+ *
+ * @param string $page the type of page. E.g. 'Admin notifications' or 'core_user > Preferences'.
+ * @return moodle_url the corresponding URL.
+ */
+ protected function resolve_page_helper(string $page): moodle_url {
+ list($component, $name) = $this->parse_page_name($page);
+ if ($component === 'core') {
+ return $this->resolve_core_page_url($name);
+ } else {
+ $context = behat_context_helper::get('behat_' . $component);
+ return $context->resolve_page_url($name);
+ }
+ }
+
+ /**
+ * Parse a full page name like 'Admin notifications' or 'core_user > Preferences'.
+ *
+ * E.g. parsing 'mod_quiz > View' gives ['mod_quiz', 'View'].
+ *
+ * @param string $page the full page name
+ * @return array with two elements, component and page name.
+ */
+ protected function parse_page_name(string $page): array {
+ $dividercount = substr_count($page, ' > ');
+ if ($dividercount === 0) {
+ return ['core', $page];
+ } else if ($dividercount === 1) {
+ list($component, $name) = explode(' > ', $page);
+ if ($component === 'core') {
+ throw new coding_exception('Do not specify the component "core > ..." for core pages.');
+ }
+ return [$component, $name];
+ } else {
+ throw new coding_exception('The page name most be in the form ' .
+ '"{page-name}" for core pages, or "{component} > {page-name}" ' .
+ 'for pages belonging to other components. ' .
+ 'For example "Admin notifications" or "mod_quiz > View".');
+ }
+ }
+
+ /**
+ * Open a given instance of a page, belonging to a plugin or core component.
+ *
+ * The instance identifier and page-type are interpreted by each plugin to
+ * work out the corresponding URL. See the resolve_page_instance_url method
+ * in each class like behat_mod_forum. That method should document which page
+ * types are recognised, and how the name identifies them.
+ *
+ * For pages belonging to core, the 'core > ' bit is omitted.
+ *
+ * @When I am on the :identifier :type page
+ * @param string $identifier identifies the particular page. E.g. 'Test quiz'.
+ * @param string $type the component and page type. E.g. 'mod_quiz > View'.
+ * @throws Exception if the specified page cannot be determined.
+ */
+ public function i_am_on_page_instance(string $identifier, string $type) {
+ $this->getSession()->visit($this->locate_path(
+ $this->resolve_page_instance_helper($identifier, $type)->out_as_local_url()));
+ }
+
+ /**
+ * Open a given page logged in as a given user.
+ *
+ * This is like the combination
+ * When I log in as "..."
+ * And I am on the "..." "..." page
+ * but with the advantage that you go straight to the desired page, without
+ * having to wait for the Dashboard to load.
+ *
+ * @When I am on the :identifier :type page logged in as :username
+ * @param string $identifier identifies the particular page. E.g. 'Test quiz'.
+ * @param string $type the component and page type. E.g. 'mod_quiz > View'.
+ * @param string $username the name of the user to log in as. E.g. 'student'.
+ * @throws Exception if the specified page cannot be determined.
+ */
+ public function i_am_on_page_instance_logged_in_as(string $identifier,
+ string $type, string $username) {
+ self::execute('behat_auth::i_log_in_as',
+ [$username, $this->resolve_page_instance_helper($identifier, $type)]);
+ }
+
+ /**
+ * Helper used by i_am_on_page() and i_am_on_page_logged_in_as().
+ *
+ * @param string $identifier identifies the particular page. E.g. 'Test quiz'.
+ * @param string $pagetype the component and page type. E.g. 'mod_quiz > View'.
+ * @return moodle_url the corresponding URL.
+ */
+ protected function resolve_page_instance_helper(string $identifier, string $pagetype): moodle_url {
+ list($component, $type) = $this->parse_page_name($pagetype);
+ if ($component === 'core') {
+ return $this->resolve_core_page_instance_url($type, $identifier);
+ } else {
+ $context = behat_context_helper::get('behat_' . $component);
+ return $context->resolve_page_instance_url($type, $identifier);
+ }
+ }
+
+ /**
+ * Convert core page names to URLs for steps like 'When I am on the "[page name]" page'.
+ *
+ * Recognised page names are:
+ * | Homepage | Homepage (normally dashboard). |
+ * | Admin notifications | Admin notification screen. |
+ *
+ * @param string $name identifies which identifies this page, e.g. 'Homepage', 'Admin notifications'.
+ * @return moodle_url the corresponding URL.
+ * @throws Exception with a meaningful error message if the specified page cannot be found.
+ */
+ protected function resolve_core_page_url(string $name): moodle_url {
+ switch ($name) {
+ case 'Homepage':
+ return new moodle_url('/');
+
+ case 'Admin notifications':
+ return new moodle_url('/admin/');
+
+ default:
+ throw new Exception('Unrecognised core page type "' . $name . '."');
+ }
+ }
+
+ /**
+ * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
+ *
+ * Recognised page names are:
+ * | Page type | Identifier meaning | description |
+ * | Category page | category idnumber | List of courses in that category. |
+ *
+ * @param string $type identifies which type of page this is, e.g. 'Category page'.
+ * @param string $identifier identifies the particular page, e.g. 'test-cat'.
+ * @return moodle_url the corresponding URL.
+ * @throws Exception with a meaningful error message if the specified page cannot be found.
+ */
+ protected function resolve_core_page_instance_url(string $type, string $identifier): moodle_url {
+ global $DB;
+
+ switch ($type) {
+ case 'Category page':
+ $categoryid = $DB->get_field('course_categories', 'id', ['idnumber' => $identifier]);
+ if (!$categoryid) {
+ throw new Exception('The specified category with idnumber "' . $identifier . '" does not exist');
+ }
+ return new moodle_url('/course/category.php', ['id' => $categoryid]);
+
+ default:
+ throw new Exception('Unrecognised core page type "' . $type . '."');
+ }
+ }
+
/**
* Opens the course homepage.
*
--- /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/>.
+
+/**
+ * Test for core_media_player_native.
+ *
+ * @package core
+ * @category test
+ * @copyright 2019 Ruslan Kabalin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once(__DIR__ . '/fixtures/testable_core_media_player_native.php');
+
+/**
+ * Test for core_media_player_native.
+ *
+ * @package core
+ * @category test
+ * @covers core_media_player_native
+ * @copyright 2019 Ruslan Kabalin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_media_player_native_testcase extends advanced_testcase {
+
+ /**
+ * Pre-test setup.
+ */
+ public function setUp() {
+ parent::setUp();
+ $this->resetAfterTest();
+ }
+
+ /**
+ * Test method get_supported_extensions
+ */
+ public function test_get_supported_extensions() {
+ global $CFG;
+ require_once($CFG->libdir . '/filelib.php');
+ $nativeextensions = file_get_typegroup('extension', ['html_video', 'html_audio']);
+
+ // Make sure that the list of extensions from the setting is exactly the same.
+ $player = new media_test_native_plugin();
+ $this->assertEmpty(array_diff($player->get_supported_extensions(), $nativeextensions));
+ $this->assertEmpty(array_diff($nativeextensions, $player->get_supported_extensions()));
+
+ }
+
+ /**
+ * Test method list_supported_urls
+ */
+ public function test_list_supported_urls() {
+ global $CFG;
+ require_once($CFG->libdir . '/filelib.php');
+ $nativeextensions = file_get_typegroup('extension', ['html_video', 'html_audio']);
+
+ // Create list of URLs for each extension.
+ $urls = array_map(function($ext){
+ return new moodle_url('http://example.org/video.' . $ext);
+ }, $nativeextensions);
+
+ // Make sure that the list of supported URLs is not filtering permitted extensions.
+ $player = new media_test_native_plugin();
+ $this->assertCount(count($urls), $player->list_supported_urls($urls));
+ }
+
+ /**
+ * Test method get_attribute
+ */
+ public function test_get_attribute() {
+ $urls = [
+ new moodle_url('http://example.org/some_filename.mp4'),
+ new moodle_url('http://example.org/some_filename_hires.mp4'),
+ ];
+
+ $player = new media_test_native_plugin();
+ // We are using fixture embed method directly as content generator.
+ $title = 'Some Filename Video';
+ $content = $player->embed($urls, $title, 0, 0, []);
+
+ $this->assertRegExp('~title="' . $title . '"~', $content);
+ $this->assertEquals($title, media_test_native_plugin::get_attribute($content, 'title'));
+ }
+
+ /**
+ * Test methods add_attributes and remove_attributes
+ */
+ public function test_add_remove_attributes() {
+ $urls = [
+ new moodle_url('http://example.org/some_filename.mp4'),
+ new moodle_url('http://example.org/some_filename_hires.mp4'),
+ ];
+
+ $player = new media_test_native_plugin();
+ // We are using fixture embed method directly as content generator.
+ $title = 'Some Filename Video';
+ $content = $player->embed($urls, $title, 0, 0, []);
+
+ // Add attributes.
+ $content = media_test_native_plugin::add_attributes($content, ['preload' => 'none', 'controls' => 'true']);
+ $this->assertRegExp('~title="' . $title . '"~', $content);
+ $this->assertRegExp('~preload="none"~', $content);
+ $this->assertRegExp('~controls="true"~', $content);
+
+ // Change existing attribute.
+ $content = media_test_native_plugin::add_attributes($content, ['controls' => 'false']);
+ $this->assertRegExp('~title="' . $title . '"~', $content);
+ $this->assertRegExp('~preload="none"~', $content);
+ $this->assertRegExp('~controls="false"~', $content);
+
+ // Remove attributes.
+ $content = media_test_native_plugin::remove_attributes($content, ['title']);
+ $this->assertNotRegExp('~title="' . $title . '"~', $content);
+ $this->assertRegExp('~preload="none"~', $content);
+ $this->assertRegExp('~controls="false"~', $content);
+
+ // Remove another one.
+ $content = media_test_native_plugin::remove_attributes($content, ['preload']);
+ $this->assertNotRegExp('~title="' . $title . '"~', $content);
+ $this->assertNotRegExp('~preload="none"~', $content);
+ $this->assertRegExp('~controls="false"~', $content);
+ }
+
+ /**
+ * Test method replace_sources
+ */
+ public function test_replace_sources() {
+ $urls = [
+ new moodle_url('http://example.org/some_filename.mp4'),
+ new moodle_url('http://example.org/some_filename_hires.mp4'),
+ ];
+
+ $player = new media_test_native_plugin();
+ // We are using fixture embed method directly as content generator.
+ $title = 'Some Filename Video';
+ $content = $player->embed($urls, $title, 0, 0, []);
+
+ // Test sources present.
+ $this->assertContains('<source src="http://example.org/some_filename.mp4" />', $content);
+ $this->assertContains('<source src="http://example.org/some_filename_hires.mp4" />', $content);
+
+ // Change sources.
+ $newsource = '<source src="http://example.org/new_filename.mp4" />';
+ $content = media_test_native_plugin::replace_sources($content, $newsource);
+ $this->assertContains($newsource, $content);
+ $this->assertNotContains('<source src="http://example.org/some_filename.mp4" />', $content);
+ $this->assertNotContains('<source src="http://example.org/some_filename_hires.mp4" />', $content);
+ }
+}
\ No newline at end of file
--- /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/>.
+
+/**
+ * Fixture for testing the functionality of core_media_player.
+ *
+ * @package core
+ * @subpackage fixtures
+ * @category test
+ * @copyright 2012 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Media player stub for testing purposes.
+ *
+ * @copyright 2012 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class media_test_plugin extends core_media_player {
+ /** @var array Array of supported extensions */
+ public $ext;
+ /** @var int Player rank */
+ public $rank;
+ /** @var int Arbitrary number */
+ public $num;
+
+ /**
+ * Constructor is used for tuning the fixture.
+ *
+ * @param int $num Number (used in output)
+ * @param int $rank Player rank
+ * @param array $ext Array of supported extensions
+ */
+ public function __construct($num = 1, $rank = 13, $ext = array('mp3', 'flv', 'f4v', 'mp4')) {
+ $this->ext = $ext;
+ $this->rank = $rank;
+ $this->num = $num;
+ }
+
+ /**
+ * Generates code required to embed the player.
+ *
+ * @param array $urls URLs of media files
+ * @param string $name Display name; '' to use default
+ * @param int $width Optional width; 0 to use default
+ * @param int $height Optional height; 0 to use default
+ * @param array $options Options array
+ * @return string HTML code for embed
+ */
+ public function embed($urls, $name, $width, $height, $options) {
+ self::pick_video_size($width, $height);
+ $contents = "\ntestsource=". join("\ntestsource=", $urls) .
+ "\ntestname=$name\ntestwidth=$width\ntestheight=$height\n<!--FALLBACK-->\n";
+ return html_writer::span($contents, 'mediaplugin mediaplugin_test');
+ }
+
+ /**
+ * Gets the list of file extensions supported by this media player.
+ *
+ * @return array Array of strings (extension not including dot e.g. '.mp3')
+ */
+ public function get_supported_extensions() {
+ return $this->ext;
+ }
+
+ /**
+ * Gets the ranking of this player.
+ *
+ * @return int Rank
+ */
+ public function get_rank() {
+ return 10;
+ }
+}
\ No newline at end of file
--- /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/>.
+
+/**
+ * Fixture for testing the functionality of core_media_player_native.
+ *
+ * @package core
+ * @subpackage fixtures
+ * @category test
+ * @copyright 2019 Ruslan Kabalin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Native media player stub for testing purposes.
+ *
+ * @copyright 2019 Ruslan Kabalin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class media_test_native_plugin extends core_media_player_native {
+ /** @var int Player rank */
+ public $rank;
+ /** @var int Arbitrary number */
+ public $num;
+
+ /**
+ * Constructor is used for tuning the fixture.
+ *
+ * @param int $num Number (used in output)
+ * @param int $rank Player rank
+ */
+ public function __construct($num = 1, $rank = 13) {
+ $this->rank = $rank;
+ $this->num = $num;
+ }
+
+ /**
+ * Generates code required to embed the player.
+ *
+ * @param array $urls URLs of media files
+ * @param string $name Display name; '' to use default
+ * @param int $width Optional width; 0 to use default
+ * @param int $height Optional height; 0 to use default
+ * @param array $options Options array
+ * @return string HTML code for embed
+ */
+ public function embed($urls, $name, $width, $height, $options) {
+ $sources = array();
+ foreach ($urls as $url) {
+ $params = ['src' => $url];
+ $sources[] = html_writer::empty_tag('source', $params);
+ }
+
+ $sources = implode("\n", $sources);
+ $title = $this->get_name($name, $urls);
+ // Escape title but prevent double escaping.
+ $title = s(preg_replace(['/&/', '/>/', '/</'], ['&', '>', '<'], $title));
+
+ return <<<OET
+<video class="mediaplugin mediaplugin_test" title="$title">
+ $sources
+</video>
+OET;
+ }
+
+ /**
+ * Gets the ranking of this player.
+ *
+ * @return int Rank
+ */
+ public function get_rank() {
+ return 10;
+ }
+}
\ No newline at end of file
/**
* Test classes for handling embedded media (audio/video).
*
- * @package core_media
- * @category phpunit
+ * @package core
+ * @category test
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
+require_once(__DIR__ . '/fixtures/testable_core_media_player.php');
/**
* Test script for media embedding.
}
return $out;
}
-}
-
-/**
- * Media player stub for testing purposes.
- */
-class media_test_plugin extends core_media_player {
- /** @var array Array of supported extensions */
- public $ext;
- /** @var int Player rank */
- public $rank;
- /** @var int Arbitrary number */
- public $num;
-
- /**
- * @param int $num Number (used in output)
- * @param int $rank Player rank
- * @param array $ext Array of supported extensions
- */
- public function __construct($num = 1, $rank = 13, $ext = array('mp3', 'flv', 'f4v', 'mp4')) {
- $this->ext = $ext;
- $this->rank = $rank;
- $this->num = $num;
- }
-
- public function embed($urls, $name, $width, $height, $options) {
- self::pick_video_size($width, $height);
- $contents = "\ntestsource=". join("\ntestsource=", $urls) .
- "\ntestname=$name\ntestwidth=$width\ntestheight=$height\n<!--FALLBACK-->\n";
- return html_writer::span($contents, 'mediaplugin mediaplugin_test');
- }
-
- public function get_supported_extensions() {
- return $this->ext;
- }
-
- public function get_rank() {
- return 10;
- }
-}
+}
\ No newline at end of file
* @copyright 2013 Damyon Wiese <damyon@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class core_tablelib_testcase extends basic_testcase {
+class core_tablelib_testcase extends advanced_testcase {
protected function generate_columns($cols) {
$columns = array();
foreach (range(0, $cols - 1) as $j) {
);
}
+ /**
+ * Data provider for test_fullname_column
+ *
+ * @return array
+ */
+ public function fullname_column_provider() {
+ return [
+ ['language'],
+ ['alternatename lastname'],
+ ['firstname lastnamephonetic'],
+ ];
+ }
+
+ /**
+ * Test fullname column observes configured alternate fullname format configuration
+ *
+ * @param string $format
+ * @return void
+ *
+ * @dataProvider fullname_column_provider
+ */
+ public function test_fullname_column(string $format) {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ set_config('alternativefullnameformat', $format);
+
+ $user = $this->getDataGenerator()->create_user();
+
+ $table = $this->create_and_setup_table(['fullname'], [], true, false, [], []);
+ $this->assertContains(fullname($user, true), $table->format_row($user)['fullname']);
+ }
+
+ /**
+ * Test fullname column ignores fullname format configuration for a user with viewfullnames capability prohibited
+ *
+ * @param string $format
+ * @return void
+ *
+ * @dataProvider fullname_column_provider
+ */
+ public function test_fullname_column_prohibit_viewfullnames(string $format) {
+ global $DB, $CFG;
+
+ $this->resetAfterTest();
+
+ set_config('alternativefullnameformat', $format);
+
+ $currentuser = $this->getDataGenerator()->create_user();
+ $this->setUser($currentuser);
+
+ // Prohibit the viewfullnames from the default user role.
+ $userrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid]);
+ role_change_permission($userrole->id, context_system::instance(), 'moodle/site:viewfullnames', CAP_PROHIBIT);
+
+ $user = $this->getDataGenerator()->create_user();
+
+ $table = $this->create_and_setup_table(['fullname'], [], true, false, [], []);
+ $this->assertContains(fullname($user, false), $table->format_row($user)['fullname']);
+ }
+
public function test_get_row_html() {
$data = $this->generate_data(1, 5);
$columns = $this->generate_columns(5);
information provided here is intended especially for developers.
=== 3.8 ===
-
+* The rotate_image function has been added to the stored_file class (MDL-63349)
* The yui checknet module is removed. Call \core\session\manager::keepalive instead.
* The generate_uuid() function has been deprecated. Please use \core\uuid::generate() instead.
* Remove lib/pear/auth/RADIUS.php (MDL-65746)
at least a single checkbox item is selected or not.
* Final deprecation (removal) of the core/modal_confirm dialogue.
* Upgrade scssphp to v1.0.2, This involves renaming classes from Leafo => ScssPhp as the repo has changed.
+* Implement supports_xsendfile() method and allow support for xsendfile in alternative_file_system_class
+ independently of local files (MDL-66304).
* The methods get_local_path_from_storedfile and get_remote_path_from_storedfile in lib/filestore/file_system.php
are now public. If you are overriding these then you will need to change your methods to public in your class.
* $CFG->httpswwwroot has been removed. It is no longer necessary as loginhttps has already been removed and it's no longer being
require_once('change_password_form.php');
require_once($CFG->libdir.'/authlib.php');
require_once($CFG->dirroot.'/webservice/lib.php');
+require_once('lib.php');
$id = optional_param('id', SITEID, PARAM_INT); // current course
$return = optional_param('return', 0, PARAM_BOOL); // redirect after password change
$strpasswordchanged = get_string('passwordchanged');
+ // Plugins can perform post password change actions once data has been validated.
+ core_login_post_change_password_requests($data);
+
$fullname = fullname($USER, true);
$PAGE->set_title($strpasswordchanged);
require_once($CFG->libdir.'/formslib.php');
require_once($CFG->dirroot.'/user/lib.php');
+require_once('lib.php');
class login_change_password_form extends moodleform {
$mform->addElement('hidden', 'id', 0);
$mform->setType('id', PARAM_INT);
+ // Hook for plugins to extend form definition.
+ core_login_extend_change_password_form($mform, $USER);
+
// buttons
if (get_user_preferences('auth_forcepasswordchange')) {
$this->add_action_buttons(false);
$errors = parent::validation($data, $files);
$reason = null;
+ // Extend validation for any form extensions from plugins.
+ $errors = array_merge($errors, core_login_validate_extend_change_password_form($data, $USER));
+
// ignore submitted username
if (!$user = authenticate_user_login($USER->username, $data['password'], true, $reason, false)) {
$errors['password'] = get_string('invalidlogin');
require_once($CFG->libdir.'/formslib.php');
require_once($CFG->dirroot.'/user/lib.php');
+require_once('lib.php');
/**
* Reset forgotten password form definition.
$mform = $this->_form;
$mform->setDisableShortforms(true);
+ // Hook for plugins to extend form definition.
+ core_login_extend_forgot_password_form($mform);
+
$mform->addElement('header', 'searchbyusername', get_string('searchbyusername'), '');
$purpose = user_edit_map_field_purpose($USER->id, 'username');
function validation($data, $files) {
$errors = parent::validation($data, $files);
+
+ // Extend validation for any form extensions from plugins.
+ $errors = array_merge($errors, core_login_validate_extend_forgot_password_form($data));
+
$errors += core_login_validate_forgot_password_data($data);
return $errors;
redirect_if_major_upgrade_required();
$testsession = optional_param('testsession', 0, PARAM_INT); // test session works properly
-$anchor = optional_param('anchor', '', PARAM_RAW); // Used to restore hash anchor to wantsurl.
+$anchor = optional_param('anchor', '', PARAM_RAW); // Used to restore hash anchor to wantsurl.
$resendconfirmemail = optional_param('resendconfirmemail', false, PARAM_BOOL);
+// It might be safe to do this for non-Behat sites, or there might
+// be a security risk. For now we only allow it on Behat sites.
+// If you wants to do the analysis, you may be able to remove the
+// if (BEHAT_SITE_RUNNING).
+if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) {
+ $wantsurl = optional_param('wantsurl', '', PARAM_LOCALURL); // Overrides $SESSION->wantsurl if given.
+ if ($wantsurl !== '') {
+ $SESSION->wantsurl = (new moodle_url($wantsurl))->out(false);
+ }
+}
+
$context = context_system::instance();
$PAGE->set_url("$CFG->wwwroot/login/index.php");
$PAGE->set_context($context);
}
list($status, $notice, $url) = core_login_process_password_reset($username, $email);
+ // Plugins can perform post forgot password actions once data has been validated.
+ core_login_post_forgot_password_requests($data);
+
// Any email has now been sent.
// Next display results to requesting user if settings permit.
echo $OUTPUT->header();
$urltogo = core_login_get_return_url();
unset($SESSION->wantsurl);
+
+ // Plugins can perform post set password actions once data has been validated.
+ core_login_post_set_password_requests($data, $user);
+
redirect($urltogo, get_string('passwordset'), 1);
}
}
}
}
}
+
+/**
+ * Plugins can extend forms.
+ */
+
+ /** Inject form elements into change_password_form.
+ * @param mform $mform the form to inject elements into.
+ * @param stdClass $user the user object to use for context.
+ */
+function core_login_extend_change_password_form($mform, $user) {
+ $callbacks = get_plugins_with_function('extend_change_password_form');
+ foreach ($callbacks as $type => $plugins) {
+ foreach ($plugins as $plugin => $pluginfunction) {
+ $pluginfunction($mform, $user);
+ }
+ }
+}
+
+ /** Inject form elements into set_password_form.
+ * @param mform $mform the form to inject elements into.
+ * @param stdClass $user the user object to use for context.
+ */
+function core_login_extend_set_password_form($mform, $user) {
+ $callbacks = get_plugins_with_function('extend_set_password_form');
+ foreach ($callbacks as $type => $plugins) {
+ foreach ($plugins as $plugin => $pluginfunction) {
+ $pluginfunction($mform, $user);
+ }
+ }
+}
+
+ /** Inject form elements into forgot_password_form.
+ * @param mform $mform the form to inject elements into.
+ */
+function core_login_extend_forgot_password_form($mform) {
+ $callbacks = get_plugins_with_function('extend_forgot_password_form');
+ foreach ($callbacks as $type => $plugins) {
+ foreach ($plugins as $plugin => $pluginfunction) {
+ $pluginfunction($mform);
+ }
+ }
+}
+
+ /** Inject form elements into signup_form.
+ * @param mform $mform the form to inject elements into.
+ */
+function core_login_extend_signup_form($mform) {
+ $callbacks = get_plugins_with_function('extend_signup_form');
+ foreach ($callbacks as $type => $plugins) {
+ foreach ($plugins as $plugin => $pluginfunction) {
+ $pluginfunction($mform);
+ }
+ }
+}
+
+/**
+ * Plugins can add additional validation to forms.
+ */
+
+/** Inject validation into change_password_form.
+ * @param array $data the data array from submitted form values.
+ * @param stdClass $user the user object to use for context.
+ * @return array $errors the updated array of errors from validation.
+ */
+function core_login_validate_extend_change_password_form($data, $user) {
+ $pluginsfunction = get_plugins_with_function('validate_extend_change_password_form');
+ $errors = array();
+ foreach ($pluginsfunction as $plugintype => $plugins) {
+ foreach ($plugins as $pluginfunction) {
+ $pluginerrors = $pluginfunction($data, $user);
+ $errors = array_merge($errors, $pluginerrors);
+ }
+ }
+ return $errors;
+}
+
+/** Inject validation into set_password_form.
+ * @param array $data the data array from submitted form values.
+ * @param stdClass $user the user object to use for context.
+ * @return array $errors the updated array of errors from validation.
+ */
+function core_login_validate_extend_set_password_form($data, $user) {
+ $pluginsfunction = get_plugins_with_function('validate_extend_set_password_form');
+ $errors = array();
+ foreach ($pluginsfunction as $plugintype => $plugins) {
+ foreach ($plugins as $pluginfunction) {
+ $pluginerrors = $pluginfunction($data, $user);
+ $errors = array_merge($errors, $pluginerrors);
+ }
+ }
+ return $errors;
+}
+
+/** Inject validation into forgot_password_form.
+ * @param array $data the data array from submitted form values.
+ * @return array $errors the updated array of errors from validation.
+ */
+function core_login_validate_extend_forgot_password_form($data) {
+ $pluginsfunction = get_plugins_with_function('validate_extend_forgot_password_form');
+ $errors = array();
+ foreach ($pluginsfunction as $plugintype => $plugins) {
+ foreach ($plugins as $pluginfunction) {
+ $pluginerrors = $pluginfunction($data);
+ $errors = array_merge($errors, $pluginerrors);
+ }
+ }
+ return $errors;
+}
+
+/** Inject validation into signup_form.
+ * @param array $data the data array from submitted form values.
+ * @return array $errors the updated array of errors from validation.
+ */
+function core_login_validate_extend_signup_form($data) {
+ $pluginsfunction = get_plugins_with_function('validate_extend_signup_form');
+ $errors = array();
+ foreach ($pluginsfunction as $plugintype => $plugins) {
+ foreach ($plugins as $pluginfunction) {
+ $pluginerrors = $pluginfunction($data);
+ $errors = array_merge($errors, $pluginerrors);
+ }
+ }
+ return $errors;
+}
+
+/**
+ * Plugins can perform post submission actions.
+ */
+
+/** Post change_password_form submission actions.
+ * @param stdClass $data the data object from the submitted form.
+ */
+function core_login_post_change_password_requests($data) {
+ $pluginsfunction = get_plugins_with_function('post_change_password_requests');
+ foreach ($pluginsfunction as $plugintype => $plugins) {
+ foreach ($plugins as $pluginfunction) {
+ $pluginfunction($data);
+ }
+ }
+}
+
+/** Post set_password_form submission actions.
+ * @param stdClass $data the data object from the submitted form.
+ * @param stdClass $user the user object for set_password context.
+ */
+function core_login_post_set_password_requests($data, $user) {
+ $pluginsfunction = get_plugins_with_function('post_set_password_requests');
+ foreach ($pluginsfunction as $plugintype => $plugins) {
+ foreach ($plugins as $pluginfunction) {
+ $pluginfunction($data, $user);
+ }
+ }
+}
+
+/** Post forgot_password_form submission actions.
+ * @param stdClass $data the data object from the submitted form.
+ */
+function core_login_post_forgot_password_requests($data) {
+ $pluginsfunction = get_plugins_with_function('post_forgot_password_requests');
+ foreach ($pluginsfunction as $plugintype => $plugins) {
+ foreach ($plugins as $pluginfunction) {
+ $pluginfunction($data);
+ }
+ }
+}
+
+/** Post signup_form submission actions.
+ * @param stdClass $data the data object from the submitted form.
+ */
+function core_login_post_signup_requests($data) {
+ $pluginsfunction = get_plugins_with_function('post_signup_requests');
+ foreach ($pluginsfunction as $plugintype => $plugins) {
+ foreach ($plugins as $pluginfunction) {
+ $pluginfunction($data);
+ }
+ }
+}
+
require_once($CFG->libdir.'/formslib.php');
require_once($CFG->dirroot.'/user/lib.php');
+require_once('lib.php');
/**
* Set forgotten password form definition.
$mform->addRule('password2', get_string('required'), 'required', null, 'client');
$mform->setType('password2', PARAM_RAW);
+ // Hook for plugins to extend form definition.
+ $user = $this->_customdata;
+ core_login_extend_set_password_form($mform, $user);
+
$this->add_action_buttons(true);
}
$errors = parent::validation($data, $files);
+ // Extend validation for any form extensions from plugins.
+ $errors = array_merge($errors, core_login_validate_extend_set_password_form($data, $user));
+
// Ignore submitted username.
if ($data['password'] !== $data['password2']) {
$errors['password'] = get_string('passwordsdiffer');
// Add missing required fields.
$user = signup_setup_new_user($user);
+ // Plugins can perform post sign up actions once data has been validated.
+ core_login_post_signup_requests($user);
+
$authplugin->user_signup($user, true); // prints notice and link to login/index.php
exit; //never reached
}
require_once($CFG->libdir.'/formslib.php');
require_once($CFG->dirroot.'/user/profile/lib.php');
require_once($CFG->dirroot . '/user/editlib.php');
+require_once('lib.php');
class login_signup_form extends moodleform implements renderable, templatable {
function definition() {
$mform->closeHeaderBefore('recaptcha_element');
}
+ // Hook for plugins to extend form definition.
+ core_login_extend_signup_form($mform);
+
// Add "Agree to sitepolicy" controls. By default it is a link to the policy text and a checkbox but
// it can be implemented differently in custom sitepolicy handlers.
$manager = new \core_privacy\local\sitepolicy\manager();
public function validation($data, $files) {
$errors = parent::validation($data, $files);
+ // Extend validation for any form extensions from plugins.
+ $errors = array_merge($errors, core_login_validate_extend_signup_form($data));
+
if (signup_captcha_enabled()) {
$recaptchaelement = $this->_form->getElement('recaptcha_element');
if (!empty($this->_form->_submitValues['g-recaptcha-response'])) {
/**
* Test method get_supported_extensions()
*/
- public function test_supported_extensions() {
+ public function test_get_supported_extensions() {
global $CFG;
require_once($CFG->libdir . '/filelib.php');
$this->assertEmpty(array_diff($nativeextensions, $player->get_supported_extensions()));
}
+ /**
+ * Test method list_supported_urls()
+ */
+ public function test_list_supported_urls() {
+ global $CFG;
+ require_once($CFG->libdir . '/filelib.php');
+
+ $nativeextensions = file_get_typegroup('extension', 'html_audio');
+
+ // Create list of URLs for each extension.
+ $urls = array_map(function($ext){
+ return new moodle_url('http://example.org/audio.' . $ext);
+ }, $nativeextensions);
+
+ // Make sure that the list of supported URLs is not filtering permitted extensions.
+ $player = new media_html5audio_plugin();
+ $this->assertCount(count($urls), $player->list_supported_urls($urls));
+ }
+
/**
* Test embedding without media filter (for example for displaying file resorce).
*/
/**
* Test method get_supported_extensions()
*/
- public function test_supported_extensions() {
+ public function test_get_supported_extensions() {
$nativeextensions = file_get_typegroup('extension', 'html_video');
// Make sure that the list of extensions from the setting is exactly the same as html_video group.
$this->assertEmpty(array_diff($nativeextensions, $player->get_supported_extensions()));
}
+ /**
+ * Test method list_supported_urls()
+ */
+ public function test_list_supported_urls() {
+ global $CFG;
+ require_once($CFG->libdir . '/filelib.php');
+
+ $nativeextensions = file_get_typegroup('extension', 'html_video');
+
+ // Create list of URLs for each extension.
+ $urls = array_map(function($ext){
+ return new moodle_url('http://example.org/video.' . $ext);
+ }, $nativeextensions);
+
+ // Make sure that the list of supported URLs is not filtering permitted extensions.
+ $player = new media_html5video_plugin();
+ $this->assertCount(count($urls), $player->list_supported_urls($urls));
+ }
+
/**
* Test embedding without media filter (for example for displaying file resorce).
*/
'core/notification',
'core/pubsub',
'core/str',
+ 'core/pending',
'core/templates',
'core/user_date',
'core_message/message_repository',
Notification,
PubSub,
Str,
+ Pending,
Templates,
UserDate,
MessageRepository,
* @return {Object} jQuery promise.
*/
var render = function(conversations, userId) {
- var formattedConversations = conversations.map(function(conversation) {
- var lastMessage = conversation.messages.length ? conversation.messages[conversation.messages.length - 1] : null;
+ // Helper to format the last message for rendering.
+ // Returns a promise which resolves to either a string, or null
+ // (such as in the event of an empty personal space).
+ var pending = new Pending();
- var formattedConversation = {
- id: conversation.id,
- imageurl: conversation.imageurl,
- name: conversation.name,
- subname: conversation.subname,
- unreadcount: conversation.unreadcount,
- ismuted: conversation.ismuted,
- lastmessagedate: lastMessage ? lastMessage.timecreated : null,
- sentfromcurrentuser: lastMessage ? lastMessage.useridfrom == userId : null,
- lastmessage: lastMessage ? $(lastMessage.text).text() || lastMessage.text : null
- };
-
- var otherUser = null;
- if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF) {
- // Self-conversations have only one member.
- otherUser = conversation.members[0];
- } else if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE) {
- // For private conversations, remove the current userId from the members to get the other user.
- otherUser = conversation.members.reduce(function(carry, member) {
- if (!carry && member.id != userId) {
- carry = member;
- }
- return carry;
- }, null);
+ var formatMessagePreview = async function(lastMessage) {
+ if (!lastMessage) {
+ return null;
+ }
+ var isMedia = lastMessage.text.includes('src');
+
+ if (!isMedia) {
+ // Try to get the text value of the content.
+ // If that's not possible, we'll report it under the catch-all 'other media'.
+ var messagePreview = $(lastMessage.text).text();
+ if (messagePreview) {
+ return messagePreview;
+ }
}
- if (otherUser !== null) {
- formattedConversation.userid = otherUser.id;
- formattedConversation.showonlinestatus = otherUser.showonlinestatus;
- formattedConversation.isonline = otherUser.isonline;
- formattedConversation.isblocked = otherUser.isblocked;
+ // As a fallback, report unknowns as 'other media' type content.
+ var pix = 'i/messagecontentmultimediageneral';
+ var label = 'messagecontentmultimediageneral';
+
+ if (lastMessage.text.includes('<img')) {
+ pix = 'i/messagecontentimage';
+ label = 'messagecontentimage';
+ } else if (lastMessage.text.includes('<video')) {
+ pix = 'i/messagecontentvideo';
+ label = 'messagecontentvideo';
+ } else if (lastMessage.text.includes('<audio')) {
+ pix = 'i/messagecontentaudio';
+ label = 'messagecontentaudio';
}
- if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PUBLIC) {
- formattedConversation.lastsendername = conversation.members.reduce(function(carry, member) {
- if (!carry && lastMessage && member.id == lastMessage.useridfrom) {
- carry = member.fullname;
- }
- return carry;
- }, null);
+ try {
+ var labelString = await Str.get_string(label, 'core_message');
+ var icon = await Templates.renderPix(pix, 'core', labelString);
+ return icon + ' ' + labelString;
+ } catch (error) {
+ Notification.exception(error);
+ return null;
}
+ };
- return formattedConversation;
- });
+ var mapPromises = conversations.map(function(conversation) {
- formattedConversations.forEach(function(conversation) {
- if (new Date().toDateString() == new Date(conversation.lastmessagedate * 1000).toDateString()) {
- conversation.istoday = true;
- }
+ var lastMessage = conversation.messages.length ? conversation.messages[conversation.messages.length - 1] : null;
+
+ return formatMessagePreview(lastMessage)
+ .then(function(messagePreview) {
+ var formattedConversation = {
+ id: conversation.id,
+ imageurl: conversation.imageurl,
+ name: conversation.name,
+ subname: conversation.subname,
+ unreadcount: conversation.unreadcount,
+ ismuted: conversation.ismuted,
+ lastmessagedate: lastMessage ? lastMessage.timecreated : null,
+ sentfromcurrentuser: lastMessage ? lastMessage.useridfrom == userId : null,
+ lastmessage: messagePreview
+ };
+
+ var otherUser = null;
+ if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF) {
+ // Self-conversations have only one member.
+ otherUser = conversation.members[0];
+ } else if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE) {
+ // For private conversations, remove the current userId from the members to get the other user.
+ otherUser = conversation.members.reduce(function(carry, member) {
+ if (!carry && member.id != userId) {
+ carry = member;
+ }
+ return carry;
+ }, null);
+ }
+
+ if (otherUser !== null) {
+ formattedConversation.userid = otherUser.id;
+ formattedConversation.showonlinestatus = otherUser.showonlinestatus;
+ formattedConversation.isonline = otherUser.isonline;
+ formattedConversation.isblocked = otherUser.isblocked;
+ }
+
+ if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PUBLIC) {
+ formattedConversation.lastsendername = conversation.members.reduce(function(carry, member) {
+ if (!carry && lastMessage && member.id == lastMessage.useridfrom) {
+ carry = member.fullname;
+ }
+ return carry;
+ }, null);
+ }
+
+ return formattedConversation;
+ }).catch(Notification.exception);
});
- return Templates.render(TEMPLATES.CONVERSATIONS_LIST, {conversations: formattedConversations});
+ return Promise.all(mapPromises)
+ .then(function(formattedConversations) {
+ formattedConversations.forEach(function(conversation) {
+ if (new Date().toDateString() == new Date(conversation.lastmessagedate * 1000).toDateString()) {
+ conversation.istoday = true;
+ }
+ });
+
+ return Templates.render(TEMPLATES.CONVERSATIONS_LIST, {conversations: formattedConversations});
+ }).then(function(html, js) {
+ pending.resolve();
+ return $.Deferred().resolve(html, js);
+ }).catch(function(error) {
+ pending.resolve();
+ Notification.exception(error);
+ });
};
/**
{{#str}} sender, core_message, {{.}} {{/str}}
{{/lastsendername}}
{{/sentfromcurrentuser}}
- <span class="text-muted">{{lastmessage}}</span>
+ <span class="text-muted">{{{lastmessage}}}</span>
</p>
</div>
<div class="d-flex align-self-stretch">
*/
class behat_message extends behat_base {
+ /**
+ * Return the list of partial named selectors.
+ *
+ * @return array
+ */
+ public static function get_partial_named_selectors(): array {
+ return [
+ new behat_component_named_selector('Message', [".//*[@data-conversation-id]//img[%altMatch%]/.."]),
+ new behat_component_named_selector('Message conversation', [
+ <<<XPATH
+ .//*[@data-region='message-drawer' and contains(., %locator%)]//div[@data-region='content-message-container']
+XPATH
+ ], false),
+ new behat_component_named_selector('Message header', [
+ <<<XPATH
+ .//*[@data-region='message-drawer']//div[@data-region='header-content' and contains(., %locator%)]
+XPATH
+ ]),
+ new behat_component_named_selector('Message member', [
+ <<<XPATH
+ .//*[@data-region='message-drawer']//div[@data-region='group-info-content-container']
+ //div[@class='list-group' and not(contains(@class, 'hidden'))]//*[%core_message/textMatch%]
+XPATH
+ , <<<XPATH
+ .//*[@data-region='message-drawer']//div[@data-region='group-info-content-container']
+ //div[@data-region='empty-message-container' and not(contains(@class, 'hidden')) and contains(., %locator%)]
+XPATH
+ ], false),
+ new behat_component_named_selector('Message tab', [
+ <<<XPATH
+ .//*[@data-region='message-drawer']//button[@data-toggle='collapse' and contains(string(), %locator%)]
+XPATH
+ ], false),
+ new behat_component_named_selector('Message list area', [
+ <<<XPATH
+ .//*[@data-region='message-drawer']//*[contains(@data-region, concat('view-overview-', %locator%))]
+XPATH
+ ], false),
+ new behat_component_named_selector('Message content', [
+ <<<XPATH
+ .//*[@data-region='message-drawer']//*[@data-region='message' and @data-message-id and contains(., %locator%)]
+XPATH
+ ], false),
+ ];
+ }
+
+ /**
+ * Return a list of the Mink named replacements for the component.
+ *
+ * Named replacements allow you to define parts of an xpath that can be reused multiple times, or in multiple
+ * xpaths.
+ *
+ * This method should return a list of {@link behat_component_named_replacement} and the docs on that class explain
+ * how it works.
+ *
+ * @return behat_component_named_replacement[]
+ */
+ public static function get_named_replacements(): array {
+ return [
+ new behat_component_named_replacement('textMatch', 'text()[contains(., %locator%)]'),
+ ];
+ }
+
/**
* Open the messaging UI.
*
public function i_open_the_conversations_list(string $tab) {
$this->execute('behat_general::i_click_on', [
$this->escape($tab),
- 'group_message_tab'
+ 'core_message > Message tab'
]);
}
$this->execute('behat_general::i_click_on',
array(
$this->escape($conversationname),
- 'group_message',
+ 'core_message > Message',
)
);
}
Scenario: Delete a message sent by the user from a group conversation
Given I log in as "student1"
And I open messaging
- And "Group 1" "group_message" should exist
+ And "Group 1" "core_message > Message" should exist
And I select "Group 1" conversation in messaging
- And I click on "Hi!" "group_message_message_content"
- And I click on "How are you?" "group_message_message_content"
- And I click on "Can somebody help me?" "group_message_message_content"
+ And I click on "Hi!" "core_message > Message content"
+ And I click on "How are you?" "core_message > Message content"
+ And I click on "Can somebody help me?" "core_message > Message content"
And I should see "3" in the "[data-region='message-selected-court']" "css_element"
# Clicking to unselect
- And I click on "How are you?" "group_message_message_content"
- And I click on "Can somebody help me?" "group_message_message_content"
+ And I click on "How are you?" "core_message > Message content"
+ And I click on "Can somebody help me?" "core_message > Message content"
And I should see "1" in the "[data-region='message-selected-court']" "css_element"
And "Delete selected messages" "button" should exist
When I click on "Delete selected messages" "button"
And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
Then I should not see "Delete"
And I should not see "Hi!"
- And I should see "##today##j F##" in the "Group 1" "group_message_conversation"
- And I should see "How are you?" in the "Group 1" "group_message_conversation"
- And I should see "Can somebody help me?" in the "Group 1" "group_message_conversation"
+ And I should see "##today##j F##" in the "Group 1" "core_message > Message conversation"
+ And I should see "How are you?" in the "Group 1" "core_message > Message conversation"
+ And I should see "Can somebody help me?" in the "Group 1" "core_message > Message conversation"
And I should not see "Messages selected"
Scenario: Delete two messages from a group conversation; one sent by another user.
Given I log in as "student1"
And I open messaging
- And "Group 1" "group_message" should exist
+ And "Group 1" "core_message > Message" should exist
And I select "Group 1" conversation in messaging
- And I click on "Hi!" "group_message_message_content"
+ And I click on "Hi!" "core_message > Message content"
And I should see "1" in the "[data-region='message-selected-court']" "css_element"
- And I click on "How are you?" "group_message_message_content"
+ And I click on "How are you?" "core_message > Message content"
And I should see "2" in the "[data-region='message-selected-court']" "css_element"
And "Delete selected messages" "button" should exist
When I click on "Delete selected messages" "button"
And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
Then I should not see "Delete"
And I should not see "Hi!"
- And I should see "##today##j F##" in the "Group 1" "group_message_conversation"
- And I should not see "How are you?" in the "Group 1" "group_message_conversation"
- And I should see "Can somebody help me?" in the "Group 1" "group_message_conversation"
+ And I should see "##today##j F##" in the "Group 1" "core_message > Message conversation"
+ And I should not see "How are you?" in the "Group 1" "core_message > Message conversation"
+ And I should see "Can somebody help me?" in the "Group 1" "core_message > Message conversation"
And I should not see "Messages selected"
# Check messages were not deleted for other users
And I log out
Scenario: Cancel deleting two messages from a group conversation
Given I log in as "student1"
And I open messaging
- And "Group 1" "group_message" should exist
+ And "Group 1" "core_message > Message" should exist
And I select "Group 1" conversation in messaging
- And I click on "Hi!" "group_message_message_content"
- And I click on "How are you?" "group_message_message_content"
+ And I click on "Hi!" "core_message > Message content"
+ And I click on "How are you?" "core_message > Message content"
And "Delete selected messages" "button" should exist
When I click on "Delete selected messages" "button"
# Canceling deletion, so messages should be there
And I click on "//button[@data-action='cancel-confirm']" "xpath_element"
Then I should not see "Cancel"
And I should see "Hi!"
- And I should see "How are you?" in the "Group 1" "group_message_conversation"
+ And I should see "How are you?" in the "Group 1" "core_message > Message conversation"
And I should see "2" in the "[data-region='message-selected-court']" "css_element"
Scenario: Delete a message sent by the user from a private conversation
And I open the "Private" conversations list
And I should see "Student 2"
And I select "Student 2" conversation in messaging
- And I click on "Hi!" "group_message_message_content"
+ And I click on "Hi!" "core_message > Message content"
And I should see "1" in the "[data-region='message-selected-court']" "css_element"
And "Delete selected messages" "button" should exist
When I click on "Delete selected messages" "button"
And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
Then I should not see "Delete"
And I should not see "Hi!"
- And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
- And I should see "Hello!" in the "Student 2" "group_message_conversation"
- And I should see "Are you free?" in the "Student 2" "group_message_conversation"
+ And I should see "##today##j F##" in the "Student 2" "core_message > Message conversation"
+ And I should see "Hello!" in the "Student 2" "core_message > Message conversation"
+ And I should see "Are you free?" in the "Student 2" "core_message > Message conversation"
And I should not see "Messages selected"
Scenario: Delete two messages from a private conversation; one sent by another user
And I open the "Private" conversations list
And I should see "Student 2"
And I select "Student 2" conversation in messaging
- And I click on "Hi!" "group_message_message_content"
+ And I click on "Hi!" "core_message > Message content"
And I should see "1" in the "[data-region='message-selected-court']" "css_element"
- And I click on "Hello!" "group_message_message_content"
+ And I click on "Hello!" "core_message > Message content"
And I should see "2" in the "[data-region='message-selected-court']" "css_element"
And "Delete selected messages" "button" should exist
When I click on "Delete selected messages" "button"
And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
Then I should not see "Delete"
And I should not see "Hi!"
- And I should not see "Hello!" in the "Student 2" "group_message_conversation"
- And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
- And I should see "Are you free?" in the "Student 2" "group_message_conversation"
+ And I should not see "Hello!" in the "Student 2" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Student 2" "core_message > Message conversation"
+ And I should see "Are you free?" in the "Student 2" "core_message > Message conversation"
And I should not see "Messages selected"
# Check messages were not deleted for the other user
And I log out
And I open the "Private" conversations list
And I should see "Student 2"
And I select "Student 2" conversation in messaging
- And I click on "Hi!" "group_message_message_content"
- And I click on "Hello!" "group_message_message_content"
+ And I click on "Hi!" "core_message > Message content"
+ And I click on "Hello!" "core_message > Message content"
And "Delete selected messages" "button" should exist
When I click on "Delete selected messages" "button"
# Canceling deletion, so messages should be there
And I click on "//button[@data-action='cancel-confirm']" "xpath_element"
Then I should not see "Cancel"
And I should see "Hi!"
- And I should see "Hello!" in the "Student 2" "group_message_conversation"
+ And I should see "Hello!" in the "Student 2" "core_message > Message conversation"
And I should see "2" in the "[data-region='message-selected-court']" "css_element"
Scenario: Delete a message sent by the user from a favorite conversation
And I open messaging
And I should see "Student 2"
And I select "Student 2" conversation in messaging
- And I click on "Hi!" "group_message_message_content"
+ And I click on "Hi!" "core_message > Message content"
And I should see "1" in the "[data-region='message-selected-court']" "css_element"
And "Delete selected messages" "button" should exist
When I click on "Delete selected messages" "button"
And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
Then I should not see "Delete"
And I should not see "Hi!"
- And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
- And I should see "Hello!" in the "Student 2" "group_message_conversation"
+ And I should see "##today##j F##" in the "Student 2" "core_message > Message conversation"
+ And I should see "Hello!" in the "Student 2" "core_message > Message conversation"
And I should not see "Messages selected"
Scenario: Delete two messages from a favourite conversation; one sent by another user
And I open messaging
And I should see "Student 2"
And I select "Student 2" conversation in messaging
- And I click on "Hi!" "group_message_message_content"
+ And I click on "Hi!" "core_message > Message content"
And I should see "1" in the "[data-region='message-selected-court']" "css_element"
- And I click on "Hello!" "group_message_message_content"
+ And I click on "Hello!" "core_message > Message content"
And I should see "2" in the "[data-region='message-selected-court']" "css_element"
And "Delete selected messages" "button" should exist
When I click on "Delete selected messages" "button"
And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
Then I should not see "Delete"
And I should not see "Hi!"
- And I should not see "Hello!" in the "Student 2" "group_message_conversation"
- And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
- And I should see "Are you free?" in the "Student 2" "group_message_conversation"
+ And I should not see "Hello!" in the "Student 2" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Student 2" "core_message > Message conversation"
+ And I should see "Are you free?" in the "Student 2" "core_message > Message conversation"
And I should not see "Messages selected"
Scenario: Cancel deleting two messages from a favourite conversation
And I open messaging
And I should see "Student 2"
And I select "Student 2" conversation in messaging
- And I click on "Hi!" "group_message_message_content"
- And I click on "Hello!" "group_message_message_content"
+ And I click on "Hi!" "core_message > Message content"
+ And I click on "Hello!" "core_message > Message content"
And "Delete selected messages" "button" should exist
When I click on "Delete selected messages" "button"
# Canceling deletion, so messages should be there
And I click on "//button[@data-action='cancel-confirm']" "xpath_element"
Then I should not see "Cancel"
And I should see "Hi!"
- And I should see "Hello!" in the "Student 2" "group_message_conversation"
+ And I should see "Hello!" in the "Student 2" "core_message > Message conversation"
And I should see "2" in the "[data-region='message-selected-court']" "css_element"
Scenario: Check an empty favourite conversation is still favourite
And I open messaging
And I should see "Student 2"
And I select "Student 2" conversation in the "favourites" conversations list
- And I click on "Hi!" "group_message_message_content"
- And I click on "Hello!" "group_message_message_content"
- And I click on "Are you free?" "group_message_message_content"
+ And I click on "Hi!" "core_message > Message content"
+ And I click on "Hello!" "core_message > Message content"
+ And I click on "Are you free?" "core_message > Message content"
And "Delete selected messages" "button" should exist
When I click on "Delete selected messages" "button"
And I should see "Delete"
Given I log in as "student1"
Then I open messaging
And I open the "Group" conversations list
- And "Group 1" "group_message" should exist
+ And "Group 1" "core_message > Message" should exist
And I select "Group 1" conversation in messaging
And I open contact menu
And I click on "Star" "link" in the "//div[@data-region='header-container']" "xpath_element"
And I go back in "view-conversation" message drawer
And I open the "Starred" conversations list
- And I should see "Group 1" in the "favourites" "group_message_list_area"
+ And I should see "Group 1" in the "favourites" "core_message > Message list area"
And I open the "Group" conversations list
- And I should not see "Group 1" in the "group-messages" "group_message_list_area"
+ And I should not see "Group 1" in the "group-messages" "core_message > Message list area"
Scenario: Unstar a group conversation
Given I log in as "student1"
Then I open messaging
And I open the "Group" conversations list
- And "Group 1" "group_message" should exist
+ And "Group 1" "core_message > Message" should exist
And I select "Group 1" conversation in messaging
And I open contact menu
And I click on "Star" "link" in the "//div[@data-region='header-container']" "xpath_element"
And I go back in "view-conversation" message drawer
And I open the "Starred" conversations list
- And I should see "Group 1" in the "favourites" "group_message_list_area"
+ And I should see "Group 1" in the "favourites" "core_message > Message list area"
And I select "Group 1" conversation in messaging
And I open contact menu
And I click on "Unstar" "link" in the "//div[@data-region='header-container']" "xpath_element"
And I go back in "view-conversation" message drawer
And I open the "Starred" conversations list
- And I should not see "Group 1" in the "favourites" "group_message_list_area"
+ And I should not see "Group 1" in the "favourites" "core_message > Message list area"
And I open the "Group" conversations list
- And I should see "Group 1" in the "group-messages" "group_message_list_area"
+ And I should see "Group 1" in the "group-messages" "core_message > Message list area"
Scenario: Star a private conversation
Given the following "private messages" exist:
Then I log in as "student1"
And I open messaging
And I open the "Private" conversations list
- And "Student 2" "group_message" should exist
+ And "Student 2" "core_message > Message" should exist
And I select "Student 2" conversation in messaging
And I open contact menu
And I click on "Star" "link" in the "//div[@data-region='header-container']" "xpath_element"
And I go back in "view-conversation" message drawer
And I open the "Starred" conversations list
- And I should see "Student 2" in the "favourites" "group_message_list_area"
+ And I should see "Student 2" in the "favourites" "core_message > Message list area"
And I open the "Private" conversations list
- And I should not see "Student 2" in the "messages" "group_message_list_area"
+ And I should not see "Student 2" in the "messages" "core_message > Message list area"
Scenario: Unstar a private conversation
Given the following "private messages" exist:
| student1 | student2 |
Then I log in as "student1"
And I open messaging
- And I should see "Student 2" in the "favourites" "group_message_list_area"
+ And I should see "Student 2" in the "favourites" "core_message > Message list area"
And I select "Student 2" conversation in messaging
And I open contact menu
And I click on "Unstar" "link" in the "//div[@data-region='header-container']" "xpath_element"
And I go back in "view-conversation" message drawer
And I open the "Starred" conversations list
- And I should not see "Group 1" in the "favourites" "group_message_list_area"
+ And I should not see "Group 1" in the "favourites" "core_message > Message list area"
And I open the "Private" conversations list
- And I should see "Student 2" in the "messages" "group_message_list_area"
\ No newline at end of file
+ And I should see "Student 2" in the "messages" "core_message > Message list area"
Given I log in as "teacher1"
Then I open messaging
And I open the "Group" conversations list
- And "Group 1" "group_message" should exist
- And "Group 2" "group_message" should exist
- And "Group 3" "group_message" should not exist
+ And "Group 1" "core_message > Message" should exist
+ And "Group 2" "core_message > Message" should exist
+ And "Group 3" "core_message > Message" should not exist
And I log out
And I log in as "student1"
And I open messaging
And I open the "Group" conversations list
- And "Group 1" "group_message" should exist
- And "Group 2" "group_message" should not exist
- And "Group 3" "group_message" should not exist
+ And "Group 1" "core_message > Message" should exist
+ And "Group 2" "core_message > Message" should not exist
+ And "Group 3" "core_message > Message" should not exist
Scenario: View group conversation's participants numbers
Given I log in as "teacher1"
Then I open messaging
And I open the "Group" conversations list
And I select "Group 1" conversation in messaging
- And I should see "5 participants" in the "Group 1" "group_message_header"
+ And I should see "5 participants" in the "Group 1" "core_message > Message header"
And I go back in "view-conversation" message drawer
And I select "Group 2" conversation in messaging
- And I should see "1 participants" in the "Group 2" "group_message_header"
+ And I should see "1 participants" in the "Group 2" "core_message > Message header"
Scenario: View group conversation's participants list
Given I log in as "teacher1"
# Check Group 1 participants list.
And I select "Group 1" conversation in messaging
And I open messaging information
- And "Teacher 1" "group_message_member" should not exist
- And "Student 0" "group_message_member" should exist
- And "Student 1" "group_message_member" should exist
- And "Student 2" "group_message_member" should exist
- And "Student 3" "group_message_member" should exist
- And "Student 4" "group_message_member" should not exist
+ And "Teacher 1" "core_message > Message member" should not exist
+ And "Student 0" "core_message > Message member" should exist
+ And "Student 1" "core_message > Message member" should exist
+ And "Student 2" "core_message > Message member" should exist
+ And "Student 3" "core_message > Message member" should exist
+ And "Student 4" "core_message > Message member" should not exist
And I go back in "group-info-content-container" message drawer
And I go back in "view-conversation" message drawer
# Check Group 2 participants list.
And I select "Group 2" conversation in messaging
And I open messaging information
- And "Teacher 1" "group_message_member" should not exist
- And "No participants" "group_message_member" should exist
- And "Student 4" "group_message_member" should not exist
+ And "Teacher 1" "core_message > Message member" should not exist
+ And "No participants" "core_message > Message member" should exist
+ And "Student 4" "core_message > Message member" should not exist
Scenario: Check group conversation members are synced when a new group member is added
Given I log in as "teacher1"
And I open messaging
And I open the "Group" conversations list
And I select "Group 1" conversation in messaging
- And I should see "6 participants" in the "Group 1" "group_message_header"
+ And I should see "6 participants" in the "Group 1" "core_message > Message header"
And I open messaging information
- And "Student 4" "group_message_member" should exist
+ And "Student 4" "core_message > Message member" should exist
And I go back in "group-info-content-container" message drawer
And I go back in "view-conversation" message drawer
And I select "Group 2" conversation in messaging
- And I should see "2 participants" in the "Group 2" "group_message_header"
+ And I should see "2 participants" in the "Group 2" "core_message > Message header"
And I open messaging information
- And "No participants" "group_message_member" should not exist
- And "Student 4" "group_message_member" should exist
+ And "No participants" "core_message > Message member" should not exist
+ And "Student 4" "core_message > Message member" should exist
And I should see "Delete"
And I click on "//button[@data-action='confirm-delete-conversation']" "xpath_element"
And I should not see "Delete"
- And I should not see "Hi!" in the "Student 1" "group_message_conversation"
- And I should not see "What do you need?" in the "Student 1" "group_message_conversation"
- And I should not see "##today##j F##" in the "Student 1" "group_message_conversation"
+ And I should not see "Hi!" in the "Student 1" "core_message > Message conversation"
+ And I should not see "What do you need?" in the "Student 1" "core_message > Message conversation"
+ And I should not see "##today##j F##" in the "Student 1" "core_message > Message conversation"
# Check user is deleting private conversation only for them
And I log out
And I log in as "student1"
And I open messaging
And I select "Student 2" conversation in the "messages" conversations list
- And I should see "Hi!" in the "Student 2" "group_message_conversation"
- And I should see "What do you need?" in the "Student 2" "group_message_conversation"
- And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
+ And I should see "Hi!" in the "Student 2" "core_message > Message conversation"
+ And I should see "What do you need?" in the "Student 2" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Student 2" "core_message > Message conversation"
Scenario: Cancel deleting a private conversation
Given I log in as "student1"
And I should see "Cancel"
And I click on "//button[@data-action='cancel-confirm']" "xpath_element"
And I should not see "Cancel"
- And I should see "Hi!" in the "Student 2" "group_message_conversation"
- And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
+ And I should see "Hi!" in the "Student 2" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Student 2" "core_message > Message conversation"
Scenario: Delete a starred conversation
Given the following "favourite conversations" exist:
And I should see "Delete"
And I click on "//button[@data-action='confirm-delete-conversation']" "xpath_element"
And I should not see "Delete"
- And I should not see "Hi!" in the "Student 2" "group_message_conversation"
- And I should not see "What do you need?" in the "Student 2" "group_message_conversation"
- And I should not see "##today##j F##" in the "Student 2" "group_message_conversation"
+ And I should not see "Hi!" in the "Student 2" "core_message > Message conversation"
+ And I should not see "What do you need?" in the "Student 2" "core_message > Message conversation"
+ And I should not see "##today##j F##" in the "Student 2" "core_message > Message conversation"
# Check user is deleting private conversation only for them
And I log out
And I log in as "student2"
And I open messaging
And I select "Student 1" conversation in the "messages" conversations list
- And I should see "Hi!" in the "Student 1" "group_message_conversation"
- And I should see "What do you need?" in the "Student 1" "group_message_conversation"
- And I should see "##today##j F##" in the "Student 1" "group_message_conversation"
+ And I should see "Hi!" in the "Student 1" "core_message > Message conversation"
+ And I should see "What do you need?" in the "Student 1" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Student 1" "core_message > Message conversation"
Scenario: Cancel deleting a starred conversation
Given the following "favourite conversations" exist:
When I log in as "student1"
And I open messaging
And I select "Student 2" conversation in the "favourites" conversations list
- Then I should see "Hi!" in the "Student 2" "group_message_conversation"
- And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
+ Then I should see "Hi!" in the "Student 2" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Student 2" "core_message > Message conversation"
And I open contact menu
And I click on "Delete conversation" "link" in the "//div[@data-region='header-container']" "xpath_element"
# Cancel deletion, so conversation should be there
And I should see "Cancel"
And I click on "//button[@data-action='cancel-confirm']" "xpath_element"
And I should not see "Cancel"
- And I should see "Hi!" in the "Student 2" "group_message_conversation"
- And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
+ And I should see "Hi!" in the "Student 2" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Student 2" "core_message > Message conversation"
Scenario: Check a deleted starred conversation is still starred
Given the following "favourite conversations" exist:
Then I should see "Delete"
And I click on "//button[@data-action='confirm-delete-conversation']" "xpath_element"
And I should not see "Delete"
- And I should not see "Hi!" in the "Student 2" "group_message_conversation"
+ And I should not see "Hi!" in the "Student 2" "core_message > Message conversation"
And I go back in "view-conversation" message drawer
- And I should not see "Student 2" in the "favourites" "group_message_list_area"
+ And I should not see "Student 2" in the "favourites" "core_message > Message list area"
And I send "Hi!" message to "Student 2" user
And I go back in "view-conversation" message drawer
And I go back in "view-search" message drawer
- And I should see "Student 2" in the "favourites" "group_message_list_area"
+ And I should see "Student 2" in the "favourites" "core_message > Message list area"
Given I log in as "student1"
And I open messaging
And I open the "Group" conversations list
- And "Group 1" "group_message" should exist
+ And "Group 1" "core_message > Message" should exist
And I select "Group 1" conversation in messaging
When I send "Hi!" message in the message area
- Then I should see "Hi!" in the "Group 1" "group_message_conversation"
- And I should see "##today##j F##" in the "Group 1" "group_message_conversation"
+ Then I should see "Hi!" in the "Group 1" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Group 1" "core_message > Message conversation"
And I log out
And I log in as "student2"
And I open messaging
- And "Group 1" "group_message" should exist
+ And "Group 1" "core_message > Message" should exist
And I select "Group 1" conversation in messaging
- And I should see "Hi!" in the "Group 1" "group_message_conversation"
+ And I should see "Hi!" in the "Group 1" "core_message > Message conversation"
Scenario: Send a message to a starred conversation
Given I log in as "student1"
When I open messaging
And I open the "Group" conversations list
- Then "Group 1" "group_message" should exist
+ Then "Group 1" "core_message > Message" should exist
And I select "Group 1" conversation in the "group-messages" conversations list
And I open contact menu
And I click on "Star" "link" in the "//div[@data-region='header-container']" "xpath_element"
And I should see "Group 1"
And I select "Group 1" conversation in the "favourites" conversations list
And I send "Hi!" message in the message area
- And I should see "Hi!" in the "Group 1" "group_message_conversation"
- And I should see "##today##j F##" in the "Group 1" "group_message_conversation"
+ And I should see "Hi!" in the "Group 1" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Group 1" "core_message > Message conversation"
And I go back in "view-conversation" message drawer
And I open the "Group" conversations list
- And I should not see "Group 1" in the "Group" "group_message_tab"
+ And I should not see "Group 1" in the "Group" "core_message > Message tab"
Scenario: Send a message to a private conversation via contact tab
Given the following "message contacts" exist:
And I click on "Contacts" "link"
And I click on "Student 2" "link" in the "//*[@data-section='contacts']" "xpath_element"
When I send "Hi!" message in the message area
- Then I should see "Hi!" in the "Student 2" "group_message_conversation"
- And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
+ Then I should see "Hi!" in the "Student 2" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Student 2" "core_message > Message conversation"
Scenario: Try to send a message to a private conversation is not contact but you are allowed to send a message
Given I log in as "student1"
And I open messaging
When I send "Hi!" message to "Student 2" user
- Then I should see "Hi!" in the "Student 2" "group_message_conversation"
- And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
+ Then I should see "Hi!" in the "Student 2" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Student 2" "core_message > Message conversation"
And I log out
And I log in as "student2"
And I open messaging
And I select "Student 1" conversation in messaging
- And I should see "Hi!" in the "Student 1" "group_message_conversation"
\ No newline at end of file
+ And I should see "Hi!" in the "Student 1" "core_message > Message conversation"
Given I log in as "student1"
When I open messaging
And I open the "Group" conversations list
- Then "Group 1" "group_message" should exist
- And "muted" "icon_container" in the "Group 1" "group_message" should not be visible
+ Then "Group 1" "core_message > Message" should exist
+ And "muted" "icon_container" in the "Group 1" "core_message > Message" should not be visible
And I select "Group 1" conversation in messaging
- And "muted" "icon_container" in the "Group 1" "group_message_header" should not be visible
+ And "muted" "icon_container" in the "Group 1" "core_message > Message header" should not be visible
And I open contact menu
And I click on "Mute" "link" in the "[data-region='header-container']" "css_element"
- And "muted" "icon_container" in the "Group 1" "group_message_header" should be visible
+ And "muted" "icon_container" in the "Group 1" "core_message > Message header" should be visible
And I go back in "view-conversation" message drawer
- And "muted" "icon_container" in the "Group 1" "group_message" should be visible
+ And "muted" "icon_container" in the "Group 1" "core_message > Message" should be visible
Scenario: Mute a private conversation
When I log in as "student1"
Then I should see "Private"
And I open the "Private" conversations list
And I should see "Student 2"
- And "muted" "icon_container" in the "Student 2" "group_message" should not be visible
+ And "muted" "icon_container" in the "Student 2" "core_message > Message" should not be visible
And I select "Student 2" conversation in messaging
And "muted" "icon_container" in the "[data-action='view-contact']" "css_element" should not be visible
And I open contact menu
And I click on "Mute" "link" in the "[data-region='header-container']" "css_element"
And "muted" "icon_container" in the "[data-action='view-contact']" "css_element" should be visible
And I go back in "view-conversation" message drawer
- And "muted" "icon_container" in the "Student 2" "group_message" should be visible
+ And "muted" "icon_container" in the "Student 2" "core_message > Message" should be visible
Scenario: Unmute a group conversation
Given the following "muted group conversations" exist:
When I log in as "student1"
And I open messaging
And I open the "Group" conversations list
- Then "Group 1" "group_message" should exist
- And "muted" "icon_container" in the "Group 1" "group_message" should be visible
+ Then "Group 1" "core_message > Message" should exist
+ And "muted" "icon_container" in the "Group 1" "core_message > Message" should be visible
And I select "Group 1" conversation in messaging
- And "muted" "icon_container" in the "Group 1" "group_message_header" should be visible
+ And "muted" "icon_container" in the "Group 1" "core_message > Message header" should be visible
And I open contact menu
And I click on "Unmute" "link" in the "[data-region='header-container']" "css_element"
- And "muted" "icon_container" in the "Group 1" "group_message_header" should not be visible
+ And "muted" "icon_container" in the "Group 1" "core_message > Message header" should not be visible
And I go back in "view-conversation" message drawer
- And "muted" "icon_container" in the "Group 1" "group_message" should not be visible
+ And "muted" "icon_container" in the "Group 1" "core_message > Message" should not be visible
Scenario: Unmute a private conversation
Given the following "muted private conversations" exist:
Then I should see "Private"
And I open the "Private" conversations list
And I should see "Student 2"
- And "muted" "icon_container" in the "Student 2" "group_message" should be visible
+ And "muted" "icon_container" in the "Student 2" "core_message > Message" should be visible
And I select "Student 2" conversation in messaging
And "muted" "icon_container" in the "[data-action='view-contact']" "css_element" should be visible
And I open contact menu
And I click on "Unmute" "link" in the "[data-region='header-container']" "css_element"
And "muted" "icon_container" in the "[data-action='view-contact']" "css_element" should not be visible
And I go back in "view-conversation" message drawer
- And "muted" "icon_container" in the "Student 2" "group_message" should not be visible
+ And "muted" "icon_container" in the "Student 2" "core_message > Message" should not be visible
Scenario: Self conversation exists
Given I log in as "student1"
When I open messaging
- Then "Student 1" "group_message" should exist
+ Then "Student 1" "core_message > Message" should exist
And I select "Student" conversation in messaging
And I should see "Personal space"
Scenario: Self conversation can be unstarred
Given I log in as "student1"
When I open messaging
- Then "Student 1" "group_message" should exist
+ Then "Student 1" "core_message > Message" should exist
And I select "Student" conversation in messaging
And I open contact menu
- And I click on "Unstar" "link" in the "Student 1" "group_message_header"
+ And I click on "Unstar" "link" in the "Student 1" "core_message > Message header"
And I go back in "view-conversation" message drawer
And I open the "Starred" conversations list
- And I should not see "Student 1" in the "favourites" "group_message_list_area"
+ And I should not see "Student 1" in the "favourites" "core_message > Message list area"
And I open the "Private" conversations list
- And I should see "Student 1" in the "messages" "group_message_list_area"
+ And I should see "Student 1" in the "messages" "core_message > Message list area"
Scenario: Self conversation can be deleted
Given I log in as "student1"
When I open messaging
- Then "Student 1" "group_message" should exist
+ Then "Student 1" "core_message > Message" should exist
And I select "Student 1" conversation in messaging
And I open contact menu
- And I click on "Delete conversation" "link" in the "Student 1" "group_message_header"
+ And I click on "Delete conversation" "link" in the "Student 1" "core_message > Message header"
And I should see "Delete"
And I click on "//button[@data-action='confirm-delete-conversation']" "xpath_element"
And I should not see "Delete"
And I go back in "view-conversation" message drawer
And I open the "Starred" conversations list
- And I should not see "Student 1" in the "favourites" "group_message_list_area"
+ And I should not see "Student 1" in the "favourites" "core_message > Message list area"
And I open the "Private" conversations list
- And I should not see "Student 1" in the "messages" "group_message_list_area"
+ And I should not see "Student 1" in the "messages" "core_message > Message list area"
Scenario: Send a message to a self-conversation via message drawer
Given I log in as "student1"
When I open messaging
- Then "Student 1" "group_message" should exist
+ Then "Student 1" "core_message > Message" should exist
And I select "Student 1" conversation in messaging
And I send "Hi!" message in the message area
- And I should see "Hi!" in the "Student 1" "group_message_conversation"
- And I should see "##today##j F##" in the "Student 1" "group_message_conversation"
+ And I should see "Hi!" in the "Student 1" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Student 1" "core_message > Message conversation"
Scenario: Send a message to a self-conversation via user profile
Given I log in as "student1"
Then I should see "Message"
And I click on "Message" "icon"
And I send "Hi!" message in the message area
- And I should see "Hi!" in the "Student 1" "group_message_conversation"
- And I should see "##today##j F##" in the "Student 1" "group_message_conversation"
+ And I should see "Hi!" in the "Student 1" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "Student 1" "core_message > Message conversation"
Given I log in as "student1"
When I open messaging
And I open the "Group" conversations list
- Then "New group" "group_message" should exist
+ Then "New group" "core_message > Message" should exist
And I select "New group" conversation in messaging
And I send "Hi!" message in the message area
- And I should see "Hi!" in the "New group" "group_message_conversation"
- And I should see "##today##j F##" in the "New group" "group_message_conversation"
+ And I should see "Hi!" in the "New group" "core_message > Message conversation"
+ And I should see "##today##j F##" in the "New group" "core_message > Message conversation"
And I log out
And I log in as "student2"
And I should see "1" in the "//*[@title='Toggle messaging drawer']/../*[@data-region='count-container']" "xpath_element"
And I open messaging
- And I should see "1" in the "Group" "group_message_tab"
- And "New group" "group_message" should exist
- And I should see "1" in the "New group" "group_message"
+ And I should see "1" in the "Group" "core_message > Message tab"
+ And "New group" "core_message > Message" should exist
+ And I should see "1" in the "New group" "core_message > Message"
And I select "New group" conversation in messaging
- And I should see "Hi!" in the "New group" "group_message_conversation"
+ And I should see "Hi!" in the "New group" "core_message > Message conversation"
And I should not see "1" in the "//*[@title='Toggle messaging drawer']/../*[@data-region='count-container']" "xpath_element"
- And I should not see "1" in the "Group" "group_message_tab"
- And I should not see "1" in the "New group" "group_message"
+ And I should not see "1" in the "Group" "core_message > Message tab"
+ And I should not see "1" in the "New group" "core_message > Message"
Scenario: Unread messages for private conversation
Given the following "private messages" exist:
When I log in as "student1"
Then I should see "1" in the "//*[@title='Toggle messaging drawer']/../*[@data-region='count-container']" "xpath_element"
And I open messaging
- And I should see "1" in the "Private" "group_message_tab"
- And "Student 2" "group_message" should exist
- And I should see "1" in the "Student 2" "group_message"
+ And I should see "1" in the "Private" "core_message > Message tab"
+ And "Student 2" "core_message > Message" should exist
+ And I should see "1" in the "Student 2" "core_message > Message"
And I select "Student 2" conversation in messaging
- And I should see "Hi!" in the "Student 2" "group_message_conversation"
+ And I should see "Hi!" in the "Student 2" "core_message > Message conversation"
And I should not see "1" in the "//*[@title='Toggle messaging drawer']/../*[@data-region='count-container']" "xpath_element"
- And I should not see "1" in the "Private" "group_message_tab"
- And I should not see "1" in the "Student 2" "group_message"
+ And I should not see "1" in the "Private" "core_message > Message tab"
+ And I should not see "1" in the "Student 2" "core_message > Message"
Scenario: Unread messages for starred conversation
Given the following "private messages" exist:
When I log in as "student1"
Then I should see "1" in the "//*[@title='Toggle messaging drawer']/../*[@data-region='count-container']" "xpath_element"
And I open messaging
- And I should see "1" in the "Starred" "group_message_tab"
- And "Student 2" "group_message" should exist
- And I should see "1" in the "Student 2" "group_message"
+ And I should see "1" in the "Starred" "core_message > Message tab"
+ And "Student 2" "core_message > Message" should exist
+ And I should see "1" in the "Student 2" "core_message > Message"
And I select "Student 2" conversation in messaging
- And I should see "Hi!" in the "Student 2" "group_message_conversation"
+ And I should see "Hi!" in the "Student 2" "core_message > Message conversation"
And I should not see "1" in the "//*[@title='Toggle messaging drawer']/../*[@data-region='count-container']" "xpath_element"
- And I should not see "1" in the "Starred" "group_message_tab"
- And I should not see "1" in the "Student 2" "group_message"
+ And I should not see "1" in the "Starred" "core_message > Message tab"
+ And I should not see "1" in the "Student 2" "core_message > Message"
const STAMPS_FILEAREA = 'stamps';
/** Filename for combined pdf */
const COMBINED_PDF_FILENAME = 'combined.pdf';
+ /** Temporary place to save JPG Image to PDF file */
+ const TMP_JPG_TO_PDF_FILEAREA = 'tmp_jpg_to_pdf';
+ /** Temporary place to save (Automatically) Rotated JPG FILE */
+ const TMP_ROTATED_JPG_FILEAREA = 'tmp_rotated_jpg';
/** Hash of blank pdf */
const BLANK_PDF_HASH = '4c803c92c71f21b423d13de570c8a09e0a31c718';
$pluginfiles = $plugin->get_files($submission, $user);
foreach ($pluginfiles as $filename => $file) {
if ($file instanceof \stored_file) {
- if ($file->get_mimetype() === 'application/pdf') {
+ $mimetype = $file->get_mimetype();
+ // PDF File, no conversion required.
+ if ($mimetype === 'application/pdf') {
$files[$filename] = $file;
- } else if ($convertedfile = $converter->start_conversion($file, 'pdf')) {
+ } else if ($plugin->allow_image_conversion() && $mimetype === "image/jpeg") {
+ // Rotates image based on the EXIF value.
+ list ($rotateddata, $size) = $file->rotate_image();
+ if ($rotateddata) {
+ $file = self::save_rotated_image_file($assignment, $userid, $attemptnumber,
+ $rotateddata, $filename);
+ }
+ // Save as PDF file if there is no available converter.
+ if (!$converter->can_convert_format_to('jpg', 'pdf')) {
+ $pdffile = self::save_jpg_to_pdf($assignment, $userid, $attemptnumber, $file, $size);
+ if ($pdffile) {
+ $files[$filename] = $pdffile;
+ }
+ }
+ }
+ // The file has not been converted to PDF, try to convert it to PDF.
+ if (!isset($files[$filename])
+ && $convertedfile = $converter->start_conversion($file, 'pdf')) {
$files[$filename] = $convertedfile;
}
} else if ($converter->can_convert_format_to('html', 'pdf')) {
}
return null;
}
+
+ /**
+ * Convert jpg file to pdf file
+ * @param int|\assign $assignment Assignment
+ * @param int $userid User ID
+ * @param int $attemptnumber Attempt Number
+ * @param \stored_file $file file to save
+ * @param null|array $size size of image
+ * @return \stored_file
+ * @throws \file_exception
+ * @throws \stored_file_creation_exception
+ */
+ private static function save_jpg_to_pdf($assignment, $userid, $attemptnumber, $file, $size=null) {
+ // Temporary file.
+ $filename = $file->get_filename();
+ $tmpdir = make_temp_directory('assignfeedback_editpdf' . DIRECTORY_SEPARATOR
+ . self::TMP_JPG_TO_PDF_FILEAREA . DIRECTORY_SEPARATOR
+ . self::hash($assignment, $userid, $attemptnumber));
+ $tempfile = $tmpdir . DIRECTORY_SEPARATOR . $filename . ".pdf";
+ // Determine orientation.
+ $orientation = 'P';
+ if (!empty($size['width']) && !empty($size['height'])) {
+ if ($size['width'] > $size['height']) {
+ $orientation = 'L';
+ }
+ }
+ // Save JPG image to PDF file.
+ $pdf = new pdf();
+ $pdf->SetHeaderMargin(0);
+ $pdf->SetFooterMargin(0);
+ $pdf->SetMargins(0, 0, 0, true);
+ $pdf->setPrintFooter(false);
+ $pdf->setPrintHeader(false);
+ $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO);
+ $pdf->AddPage($orientation);
+ $pdf->SetAutoPageBreak(false);
+ // Width has to be define here to fit into A4 page. Otherwise the image will be inserted with original size.
+ if ($orientation == 'P') {
+ $pdf->Image('@' . $file->get_content(), 0, 0, 210);
+ } else {
+ $pdf->Image('@' . $file->get_content(), 0, 0, 297);
+ }
+ $pdf->setPageMark();
+ $pdf->save_pdf($tempfile);
+ $filearea = self::TMP_JPG_TO_PDF_FILEAREA;
+ $pdffile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile);
+ if (file_exists($tempfile)) {
+ unlink($tempfile);
+ rmdir($tmpdir);
+ }
+ return $pdffile;
+ }
+
+ /**
+ * Save rotated image data to file.
+ * @param int|\assign $assignment Assignment
+ * @param int $userid User ID
+ * @param int $attemptnumber Attempt Number
+ * @param resource $rotateddata image data to save
+ * @param string $filename name of the image file
+ * @return \stored_file
+ * @throws \file_exception
+ * @throws \stored_file_creation_exception
+ */
+ private static function save_rotated_image_file($assignment, $userid, $attemptnumber, $rotateddata, $filename) {
+ $filearea = self::TMP_ROTATED_JPG_FILEAREA;
+ $tmpdir = make_temp_directory('assignfeedback_editpdf' . DIRECTORY_SEPARATOR
+ . $filearea . DIRECTORY_SEPARATOR
+ . self::hash($assignment, $userid, $attemptnumber));
+ $tempfile = $tmpdir . DIRECTORY_SEPARATOR . basename($filename);
+ imagejpeg($rotateddata, $tempfile);
+ $newfile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile);
+ if (file_exists($tempfile)) {
+ unlink($tempfile);
+ rmdir($tmpdir);
+ }
+ return $newfile;
+ }
+
}
return $sets;
}
+
+ /**
+ * Determine if the plugin allows image file conversion
+ * @return bool
+ */
+ public function allow_image_conversion() {
+ return true;
+ }
}
public function submission_is_empty(stdClass $data) {
return false;
}
+
+ /**
+ * Determine if the plugin allows image file conversion
+ * @return bool
+ */
+ public function allow_image_conversion() {
+ return false;
+ }