"plusplus": false,
"predef": [
"M",
- "define"
+ "define",
+ "require"
],
"proto": false,
"regexdash": false,
// set up language
$lang = clean_param($options['lang'], PARAM_SAFEDIR);
-if (file_exists($CFG->dirroot.'/install/lang/'.$lang)) {
+$languages = get_string_manager()->get_list_of_translations();
+if (array_key_exists($lang, $languages)) {
$CFG->lang = $lang;
}
//Fist select language
if ($interactive) {
cli_separator();
- $languages = get_string_manager()->get_list_of_translations();
// Do not put the langs into columns because it is not compatible with RTL.
- $langlist = implode("\n", $languages);
$default = $CFG->lang;
- cli_heading(get_string('availablelangs', 'install'));
- echo $langlist."\n";
+ cli_heading(get_string('chooselanguagehead', 'install'));
+ if (array_key_exists($default, $languages)) {
+ echo $default.' - '.$languages[$default]."\n";
+ }
+ if ($default !== 'en') {
+ echo 'en - English (en)'."\n";
+ }
+ echo '? - '.get_string('availablelangs', 'install')."\n";
$prompt = get_string('clitypevaluedefault', 'admin', $CFG->lang);
$error = '';
do {
echo $error;
$input = cli_input($prompt, $default);
- $input = clean_param($input, PARAM_SAFEDIR);
- if (!file_exists($CFG->dirroot.'/install/lang/'.$input)) {
- $error = get_string('cliincorrectvalueretry', 'admin')."\n";
+ if ($input === '?') {
+ echo implode("\n", $languages)."\n";
+ $error = "\n";
+
} else {
- $error = '';
+ $input = clean_param($input, PARAM_SAFEDIR);
+
+ if (!array_key_exists($input, $languages)) {
+ $error = get_string('cliincorrectvalueretry', 'admin')."\n";
+ } else {
+ $error = '';
+ }
}
} while ($error !== '');
$CFG->lang = $input;
$predbqueries = $DB->perf_get_queries();
$pretime = microtime(true);
- mtrace("Scheduled task: " . $task->get_name());
+ $fullname = $task->get_name() . ' (' . get_class($task) . ')';
+ mtrace('Execute scheduled task: ' . $fullname);
// NOTE: it would be tricky to move this code to \core\task\manager class,
// because we want to do detailed error reporting.
$cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
mtrace("... used " . (microtime(1) - $pretime) . " seconds");
}
- mtrace("Task completed.");
+ mtrace('Scheduled task complete: ' . $fullname);
\core\task\manager::scheduled_task_complete($task);
get_mailer('close');
exit(0);
}
mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
mtrace("... used " . (microtime(true) - $pretime) . " seconds");
- mtrace("Task failed: " . $e->getMessage());
+ mtrace('Scheduled task failed: ' . $fullname . ',' . $e->getMessage());
if ($CFG->debugdeveloper) {
if (!empty($e->debuginfo)) {
mtrace("Debug info:");
}
if (context) {
templates.render(templateName, context).done(function(html, js) {
- $('[data-region="displaytemplateexample"]').empty();
- $('[data-region="displaytemplateexample"]').append(html);
- templates.runTemplateJS(js);
+ templates.replaceNodeContents($('[data-region="displaytemplateexample"]'), html, js);
}).fail(notification.exception);
} else {
str.get_string('templatehasnoexample', 'tool_templatelibrary').done(function(s) {
*/
var reloadListTemplate = function(templateList) {
templates.render('tool_templatelibrary/search_results', { templates: templateList })
- .done(function (result) {
- $('[data-region="searchresults"]').replaceWith(result);
+ .done(function (result, js) {
+ templates.replaceNode($('[data-region="searchresults"]'), result, js);
}).fail(notification.exception);
};
echo $OUTPUT->header();
$fullname = fullname($user, true);
echo $OUTPUT->heading(get_string('deleteuser', 'admin'));
+
$optionsyes = array('delete'=>$delete, 'confirm'=>md5($delete), 'sesskey'=>sesskey());
- echo $OUTPUT->confirm(get_string('deletecheckfull', '', "'$fullname'"), new moodle_url($returnurl, $optionsyes), $returnurl);
+ $deleteurl = new moodle_url($returnurl, $optionsyes);
+ $deletebutton = new single_button($deleteurl, get_string('delete'), 'post');
+
+ echo $OUTPUT->confirm(get_string('deletecheckfull', '', "'$fullname'"), $deletebutton, $returnurl);
echo $OUTPUT->footer();
die;
} else if (data_submitted() and !$user->deleted) {
$message = "{$strdeletecoursecheck}<br /><br />{$coursefullname} ({$courseshortname})";
$continueurl = new moodle_url('/course/delete.php', array('id' => $course->id, 'delete' => md5($course->timemodified)));
+$continuebutton = new single_button($continueurl, get_string('delete'), 'post');
$PAGE->navbar->add($strdeletecheck);
$PAGE->set_title("$SITE->shortname: $strdeletecheck");
$PAGE->set_heading($SITE->fullname);
echo $OUTPUT->header();
-echo $OUTPUT->confirm($message, $continueurl, $categoryurl);
+echo $OUTPUT->confirm($message, $continuebutton, $categoryurl);
echo $OUTPUT->footer();
-exit;
\ No newline at end of file
+exit;
echo $OUTPUT->box_start('noticebox');
$optionsyes = array('id' => $id, 'confirm' => 1, 'delete' => 1, 'sesskey' => sesskey());
$deleteurl = new moodle_url('/course/editsection.php', $optionsyes);
- $formcontinue = new single_button($deleteurl, get_string('continue'));
+ $formcontinue = new single_button($deleteurl, get_string('delete'));
$formcancel = new single_button($cancelurl, get_string('cancel'), 'get');
echo $OUTPUT->confirm(get_string('confirmdeletesection', '',
get_section_name($course, $sectioninfo)), $formcontinue, $formcancel);
Scenario: Deleting the last section in topics format
When I click on "Delete topic" "link" in the "li#section-5" "css_element"
Then I should see "Are you absolutely sure you want to completely delete \"Topic 5\" and all the activities it contains?"
- And I press "Continue"
+ And I press "Delete"
And I should not see "Topic 5"
And I navigate to "Edit settings" node in "Course administration"
And I expand all fieldsets
Scenario: Deleting the middle section in topics format
When I click on "Delete topic" "link" in the "li#section-4" "css_element"
- And I press "Continue"
+ And I press "Delete"
Then I should not see "Topic 5"
And I should not see "Test chat name"
And I should see "Test choice name" in the "li#section-4" "css_element"
When I follow "Reduce the number of sections"
Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
And I click on "Delete topic" "link" in the "li#section-5" "css_element"
- And I press "Continue"
+ And I press "Delete"
And I should not see "Topic 5"
And I should not see "Orphaned activities"
And "li#section-5" "css_element" should not exist
And "li#section-5.orphaned" "css_element" should exist
And "li#section-4.orphaned" "css_element" should not exist
And I click on "Delete topic" "link" in the "li#section-1" "css_element"
- And I press "Continue"
+ And I press "Delete"
And I should not see "Test book name"
And I should see "Orphaned activities (section 4)" in the "li#section-4" "css_element"
And "li#section-5" "css_element" should not exist
Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
When I click on "Delete week" "link" in the "li#section-5" "css_element"
Then I should see "Are you absolutely sure you want to completely delete \"29 May - 4 June\" and all the activities it contains?"
- And I press "Continue"
+ And I press "Delete"
And I should not see "29 May - 4 June"
And I navigate to "Edit settings" node in "Course administration"
And I expand all fieldsets
Scenario: Deleting the middle section in weeks format
Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
When I click on "Delete week" "link" in the "li#section-4" "css_element"
- And I press "Continue"
+ And I press "Delete"
Then I should not see "29 May - 4 June"
And I should not see "Test chat name"
And I should see "Test choice name" in the "li#section-4" "css_element"
When I follow "Reduce the number of sections"
Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
And I click on "Delete week" "link" in the "li#section-5" "css_element"
- And I press "Continue"
+ And I press "Delete"
And I should not see "29 May - 4 June"
And I should not see "Orphaned activities"
And "li#section-5" "css_element" should not exist
And "li#section-5.orphaned" "css_element" should exist
And "li#section-4.orphaned" "css_element" should not exist
And I click on "Delete week" "link" in the "li#section-1" "css_element"
- And I press "Continue"
+ And I press "Delete"
And I should not see "Test book name"
And I should see "Orphaned activities (section 4)" in the "li#section-4" "css_element"
And "li#section-5" "css_element" should not exist
# Redirect
And I should see "Delete TCCAC"
And I should see "Test course: create a course (TCCAC)"
- And I press "Continue"
+ And I press "Delete"
# Redirect
And I should see "Deleting TCCAC"
And I should see "TCCAC has been completely deleted"
# Redirect
And I should see "Delete TCCAC"
And I should see "Test course: create a course (TCCAC)"
- And I press "Continue"
+ And I press "Delete"
# Redirect
And I should see "Deleting TCCAC"
And I should see "TCCAC has been completely deleted"
$notify = false;
if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$user->id))) {
// Update only.
- $this->update_user_enrol($instance, $user->id, ENROL_USER_ACTIVE, $roleid, $timestart, $timeend);
+ $this->update_user_enrol($instance, $user->id, ENROL_USER_ACTIVE, $timestart, $timeend);
if (!$DB->record_exists('role_assignments', array('contextid'=>$context->id, 'roleid'=>$roleid, 'userid'=>$user->id, 'component'=>'enrol_flatfile', 'itemid'=>$instance->id))) {
role_assign($roleid, $user->id, $context->id, 'enrol_flatfile', $instance->id);
}
--- /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/>.
+
+/**
+ * Empty enrol_self form.
+ *
+ * Useful to mimic valid enrol instances UI when the enrolment instance is not available.
+ *
+ * @package enrol_self
+ * @copyright 2015 David Monllaó
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+class enrol_self_empty_form extends moodleform {
+
+ /**
+ * Form definition.
+ * @return void
+ */
+ public function definition() {
+ $this->_form->addElement('header', 'selfheader', $this->_customdata->header);
+ $this->_form->addElement('static', 'info', '', $this->_customdata->info);
+ }
+}
$enrolstatus = $this->can_self_enrol($instance);
- // Don't show enrolment instance form, if user can't enrol using it.
if (true === $enrolstatus) {
+ // This user can self enrol using this instance.
$form = new enrol_self_enrol_form(NULL, $instance);
$instanceid = optional_param('instance', 0, PARAM_INT);
if ($instance->id == $instanceid) {
$this->enrol_self($instance, $data);
}
}
-
- ob_start();
- $form->display();
- $output = ob_get_clean();
- return $OUTPUT->box($output);
} else {
- return $OUTPUT->box($enrolstatus);
- }
+ // This user can not self enrol using this instance. Using an empty form to keep
+ // the UI consistent with other enrolment plugins that returns a form.
+ $data = new stdClass();
+ $data->header = $this->get_instance_name($instance);
+ $data->info = $enrolstatus;
+ $form = new enrol_self_empty_form(null, $data);
+ }
+
+ ob_start();
+ $form->display();
+ $output = ob_get_clean();
+ return $OUTPUT->box($output);
}
/**
alertpanels: {},
initializer : function() {
var self = this;
- Y.delegate('click', function(e){
- e.preventDefault();
+ require(['core/event'], function(event) {
+ Y.delegate('click', function(e){
+ e.preventDefault();
- //display a progress indicator
- var title = '',
- content = Y.Node.create('<div id="glossaryfilteroverlayprogress">' +
- '<img src="' + M.cfg.loadingicon + '" class="spinner" />' +
- '</div>'),
- o = new Y.Overlay({
- headerContent : title,
- bodyContent : content
- }),
- fullurl,
- cfg;
- self.overlay = o;
- o.render(Y.one(document.body));
+ //display a progress indicator
+ var title = '',
+ content = Y.Node.create('<div id="glossaryfilteroverlayprogress">' +
+ '<img src="' + M.cfg.loadingicon + '" class="spinner" />' +
+ '</div>'),
+ o = new Y.Overlay({
+ headerContent : title,
+ bodyContent : content
+ }),
+ fullurl,
+ cfg;
+ self.overlay = o;
+ o.render(Y.one(document.body));
- //Switch over to the ajax url and fetch the glossary item
- fullurl = this.getAttribute('href').replace('showentry.php','showentry_ajax.php');
- cfg = {
- method: 'get',
- context : self,
- on: {
- success: function(id, o) {
- this.display_callback(o.responseText);
- },
- failure: function(id, o) {
- var debuginfo = o.statusText;
- if (M.cfg.developerdebug) {
- o.statusText += ' (' + fullurl + ')';
+ //Switch over to the ajax url and fetch the glossary item
+ fullurl = this.getAttribute('href').replace('showentry.php','showentry_ajax.php');
+ cfg = {
+ method: 'get',
+ context : self,
+ on: {
+ success: function(id, o) {
+ this.display_callback(o.responseText, event);
+ },
+ failure: function(id, o) {
+ var debuginfo = o.statusText;
+ if (M.cfg.developerdebug) {
+ o.statusText += ' (' + fullurl + ')';
+ }
+ new M.core.exception({ message: debuginfo });
}
- this.display_callback('bodyContent',debuginfo);
}
- }
- };
- Y.io(fullurl, cfg);
+ };
+ Y.io(fullurl, cfg);
- }, Y.one(document.body), 'a.glossary.autolink.concept');
+ }, Y.one(document.body), 'a.glossary.autolink.concept');
+ });
},
- display_callback : function(content) {
+ /**
+ * @method display_callback
+ * @param {String} content - Content to display
+ * @param {Object} event The amd event module used to fire events for jquery and yui.
+ */
+ display_callback : function(content, event) {
var data,
key,
alertpanel,
definition = data.entries[key].definition + data.entries[key].attachments;
alertpanel = new M.core.alert({title:data.entries[key].concept, draggable: true,
message:definition, modal:false, yesLabel: M.util.get_string('ok', 'moodle')});
- Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(alertpanel.get('boundingBox')))});
+ // Notify the filters about the modified nodes.
+ event.notifyFilterContentUpdated(alertpanel.get('boundingBox').getDOMNode());
Y.Node.one('#id_yuialertconfirm-' + alertpanel.get('COUNT')).focus();
// Register alertpanel for stacking.
$string['defaultpage'] = 'Default My Moodle page';
$string['defaultprofilepage'] = 'Default profile page';
$string['addpage'] = 'Add page';
+$string['alldashboardswerereset'] = 'All Dashboard pages have been reset to default.';
+$string['allprofileswerereset'] = 'All profile pages have been reset to default.';
$string['delpage'] = 'Delete page';
$string['managepages'] = 'Manage pages';
+$string['reseteveryonesdashboard'] = 'Reset Dashboard for all users';
+$string['reseteveryonesprofile'] = 'Reset profile for all users';
$string['resetpage'] = 'Reset page to default';
$string['reseterror'] = 'There was an error resetting your page';
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Global registry of core events that can be triggered/listened for.
+ *
+ * @module core/event
+ * @package core
+ * @class event
+ * @copyright 2015 Damyon Wiese <damyon@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since 3.0
+ */
+define([ 'jquery', 'core/yui' ],
+ function($, Y) {
+
+ return /** @alias module:core/event */ {
+ // Public variables and functions.
+ /**
+ * Trigger an event using both JQuery and YUI
+ *
+ * @method notifyFilterContentUpdated
+ * @param {string}|{JQuery} nodes - Selector or list of elements that were inserted.
+ */
+ notifyFilterContentUpdated: function(nodes) {
+ nodes = $(nodes);
+ Y.use('event', 'moodle-core-event', function(Y) {
+ // Trigger it the JQuery way.
+ $('document').trigger(M.core.event.FILTER_CONTENT_UPDATED, nodes);
+
+ // Create a YUI NodeList from our JQuery Object.
+ var yuiNodes = new Y.NodeList(nodes.get());
+
+ // And again for YUI.
+ Y.fire(M.core.event.FILTER_CONTENT_UPDATED, { nodes: yuiNodes });
+ });
+ },
+
+ };
+});
* Because every module is returned from a request for any other module, this
* forces the loading of all modules with a single request.
*
+ * This function also sets up the listeners for ajax requests so we can tell
+ * if any requests are still in progress.
+ *
* @module core/first
* @package core
* @copyright 2015 Damyon Wiese <damyon@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 2.9
*/
-define(function() { });
+define(['jquery'], function($) {
+ $(document).bind("ajaxStart", function(){
+ M.util.js_pending('jq');
+ }).bind("ajaxStop", function(){
+ M.util.js_complete('jq');
+ });
+});
'core/notification',
'core/url',
'core/config',
- 'core/localstorage'
+ 'core/localstorage',
+ 'core/event'
],
- function(mustache, $, ajax, str, notification, coreurl, config, storage) {
+ function(mustache, $, ajax, str, notification, coreurl, config, storage, event) {
// Private variables and functions.
return deferred.promise();
};
+ /**
+ * Execute a block of JS returned from a template.
+ * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
+ *
+ * @method runTemplateJS
+ * @param {string} source - A block of javascript.
+ */
+ var runTemplateJS = function(source) {
+ if (source.trim() !== '') {
+ var newscript = $('<script>').attr('type','text/javascript').html(source);
+ $('head').append(newscript);
+ }
+ };
+
+ /**
+ * Do some DOM replacement and trigger correct events and fire javascript.
+ *
+ * @method domReplace
+ * @private
+ * @param {JQuery} element - Element or selector to replace.
+ * @param {String} newHTML - HTML to insert / replace.
+ * @param {String} newJS - Javascript to run after the insertion.
+ * @param {Boolean} replaceChildNodes - Replace only the childnodes, alternative is to replace the entire node.
+ */
+ var domReplace = function(element, newHTML, newJS, replaceChildNodes) {
+ var replaceNode = $(element);
+ if (replaceNode.length) {
+ // First create the dom nodes so we have a reference to them.
+ var newNodes = $(newHTML);
+ // Do the replacement in the page.
+ if (replaceChildNodes) {
+ replaceNode.empty();
+ replaceNode.append(newNodes);
+ } else {
+ replaceNode.replaceWith(newNodes);
+ }
+ // Run any javascript associated with the new HTML.
+ runTemplateJS(newJS);
+ // Notify all filters about the new content.
+ event.notifyFilterContentUpdated(newNodes);
+ }
+ };
+
+
return /** @alias module:core/templates */ {
// Public variables and functions.
/**
* Call this AFTER adding the template HTML into the DOM so the nodes can be found.
*
* @method runTemplateJS
- * @private
* @param {string} source - A block of javascript.
*/
- runTemplateJS: function(source) {
- var newscript = $('<script>').attr('type','text/javascript').html(source);
- $('head').append(newscript);
+ runTemplateJS: runTemplateJS,
+
+ /**
+ * Replace a node in the page with some new HTML and run the JS.
+ *
+ * @method replaceNodeContents
+ * @param {string} source - A block of javascript.
+ */
+ replaceNodeContents: function(element, newHTML, newJS) {
+ return domReplace(element, newHTML, newJS, true);
+ },
+
+ /**
+ * Insert a node in the page with some new HTML and run the JS.
+ *
+ * @method replaceNode
+ * @param {string} source - A block of javascript.
+ */
+ replaceNode: function(element, newHTML, newJS) {
+ return domReplace(element, newHTML, newJS, false);
}
};
});
}
}
+/**
+ * Delete multiple blocks at once.
+ *
+ * @param array $instanceids A list of block instance ID.
+ */
+function blocks_delete_instances($instanceids) {
+ global $DB;
+
+ $instances = $DB->get_recordset_list('block_instances', 'id', $instanceids);
+ foreach ($instances as $instance) {
+ blocks_delete_instance($instance, false, true);
+ }
+ $instances->close();
+
+ $DB->delete_records_list('block_positions', 'blockinstanceid', $instanceids);
+ $DB->delete_records_list('block_instances', 'id', $instanceids);
+
+ $preferences = array();
+ foreach ($instanceids as $instanceid) {
+ $preferences[] = 'block' . $instanceid . 'hidden';
+ $preferences[] = 'docked_block_instance_' . $instanceid;
+ }
+ $DB->delete_records_list('user_preferences', 'name', $preferences);
+}
+
/**
* Delete all the blocks that belong to a particular context.
*
* @return array message and message format to use.
*/
protected static function remove_quoted_text($messagedata) {
- $linecount = self::get_linecount_to_remove($messagedata);
if (!empty($messagedata->plain)) {
$text = $messagedata->plain;
} else {
return array($text, $messageformat);
}
- // Remove extra line. "Xyz wrote on...".
- $count = 0;
$i = 0;
$flag = false;
foreach ($splitted as $i => $element) {
$element = $splitted[$j];
if (!empty($element)) {
unset($splitted[$j]);
- $count++;
- }
- if ($count == $linecount) {
break;
}
}
}
if ($flag) {
// Quoted text was found.
- $k = $i - $linecount; // Where to start the chopping process.
-
- // Remove quoted text.
- $splitted = array_slice($splitted, 0, $k);
+ // Retrieve everything from the start until the line before the quoted text.
+ $splitted = array_slice($splitted, 0, $i-1);
// Strip out empty lines towards the end, since a lot of clients add a huge chunk of empty lines.
$reverse = array_reverse($splitted);
}
return array($message, $messageformat);
}
-
- /**
- * Try to guess how many lines to remove from the email to delete "xyz wrote on" text. Hard coded numbers for various email
- * clients.
- * Gmail uses two
- * Evolution uses one
- * Thunderbird uses one
- *
- * @param \stdClass $messagedata The Inbound Message record
- *
- * @return int number of lines to chop off before the start of quoted text.
- */
- protected static function get_linecount_to_remove($messagedata) {
- $linecount = 1;
- if (!empty($messagedata->html) && stripos($messagedata->html, 'gmail_quote') !== false) {
- // Gmail uses two lines.
- $linecount = 2;
- }
- return $linecount;
- }
}
// Run all scheduled tasks.
while (!\core\task\manager::static_caches_cleared_since($timenow) &&
$task = \core\task\manager::get_next_scheduled_task($timenow)) {
- mtrace("Execute scheduled task: " . $task->get_name());
+ $fullname = $task->get_name() . ' (' . get_class($task) . ')';
+ mtrace('Execute scheduled task: ' . $fullname);
cron_trace_time_and_memory();
$predbqueries = null;
$predbqueries = $DB->perf_get_queries();
mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
mtrace("... used " . (microtime(1) - $pretime) . " seconds");
}
- mtrace("Scheduled task complete: " . $task->get_name());
+ mtrace('Scheduled task complete: ' . $fullname);
\core\task\manager::scheduled_task_complete($task);
} catch (Exception $e) {
if ($DB && $DB->is_transaction_started()) {
mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
mtrace("... used " . (microtime(1) - $pretime) . " seconds");
}
- mtrace("Scheduled task failed: " . $task->get_name() . "," . $e->getMessage());
+ mtrace('Scheduled task failed: ' . $fullname . ',' . $e->getMessage());
if ($CFG->debugdeveloper) {
if (!empty($e->debuginfo)) {
mtrace("Debug info:");
<?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20150824" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20150922" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
<KEY NAME="createdby" TYPE="foreign" FIELDS="createdby" REFTABLE="user" REFFIELDS="id" COMMENT="foreign (createdby) references user (id)"/>
<KEY NAME="modifiedby" TYPE="foreign" FIELDS="modifiedby" REFTABLE="user" REFFIELDS="id" COMMENT="foreign (modifiedby) references user (id)"/>
</KEYS>
+ <INDEXES>
+ <INDEX NAME="qtype" UNIQUE="false" FIELDS="qtype"/>
+ </INDEXES>
</TABLE>
<TABLE NAME="question_answers" COMMENT="Answers, with a fractional grade (0-1) and feedback">
<FIELDS>
upgrade_main_savepoint(true, 2015090801.00);
}
+ if ($oldversion < 2015092200.00) {
+ // Define index qtype (not unique) to be added to question.
+ $table = new xmldb_table('question');
+ $index = new xmldb_index('qtype', XMLDB_INDEX_NOTUNIQUE, array('qtype'));
+
+ // Conditionally launch add index qtype.
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2015092200.00);
+ }
+
return true;
}
/**
* Returns 'LIKE' part of a query.
*
+ * Note that mysql does not support $casesensitive = true and $accentsensitive = false.
+ * More information in http://bugs.mysql.com/bug.php?id=19567.
+ *
* @param string $fieldname usually name of the table column
* @param string $param usually bound query parameter (?, :named)
* @param bool $casesensitive use case sensitive search
- * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
+ * @param bool $accensensitive use accent sensitive search (ignored if $casesensitive is true)
* @param bool $notlike true means "NOT LIKE"
* @param string $escapechar escape char for '%' and '_'
* @return string SQL code fragment
$escapechar = $this->mysqli->real_escape_string($escapechar); // prevents problems with C-style escapes of enclosing '\'
$LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
+
if ($casesensitive) {
+ // Current MySQL versions do not support case sensitive and accent insensitive.
return "$fieldname $LIKE $param COLLATE utf8_bin ESCAPE '$escapechar'";
+
+ } else if ($accentsensitive) {
+ // Case insensitive and accent sensitive, we can force a binary comparison once all texts are using the same case.
+ return "LOWER($fieldname) $LIKE LOWER($param) COLLATE utf8_bin ESCAPE '$escapechar'";
+
} else {
- if ($accentsensitive) {
- return "LOWER($fieldname) $LIKE LOWER($param) COLLATE utf8_bin ESCAPE '$escapechar'";
- } else {
- return "$fieldname $LIKE $param ESCAPE '$escapechar'";
+ // Case insensitive and accent insensitive.
+ $collation = '';
+ if ($this->get_dbcollation() == 'utf8_bin') {
+ // Force a case insensitive comparison if using utf8_bin.
+ $collation = 'COLLATE utf8_unicode_ci';
}
+
+ return "$fieldname $LIKE $param $collation ESCAPE '$escapechar'";
}
}
$records = $DB->get_records_sql($sql, array('aui'));
$this->assertCount(1, $records);
+ // Test LIKE under unusual collations.
+ $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', false, false);
+ $records = $DB->get_records_sql($sql, array("%dup_r%"));
+ $this->assertCount(2, $records);
+
$sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', true, true, true); // NOT LIKE.
$records = $DB->get_records_sql($sql, array("%o%"));
$this->assertCount(3, $records);
* @class Button
* @extends M.editor_atto.EditorPlugin
*/
-
var COMPONENTNAME = 'atto_equation',
LOGNAME = 'atto_equation',
CSS = {
tabview.render();
dialogue.show();
- // Trigger any JS filters to reprocess the new nodes.
- Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(dialogue.get('boundingBox')))});
+ // Notify the filters about the modified nodes.
+ require(['core/event'], function(event) {
+ event.notifyFilterContentUpdated(dialogue.get('boundingBox').getDOMNode());
+ });
if (equation) {
content.one(SELECTORS.EQUATION_TEXT).set('text', equation);
if (preview.status === 200) {
previewNode.setHTML(preview.responseText);
- Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(previewNode))});
+ // Notify the filters about the modified nodes.
+ require(['core/event'], function(event) {
+ event.notifyFilterContentUpdated(previewNode.getDOMNode());
+ });
}
},
};
var resize_object = function() {
- obj.setStyle('width', '0px');
- obj.setStyle('height', '0px');
+ obj.setStyle('display', 'none');
var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
if (newwidth > 500) {
newheight = 400;
}
obj.setStyle('height', newheight+'px');
+ obj.setStyle('display', '');
};
resize_object();
// fix layout if window resized too
- window.onresize = function() {
- resize_object();
- };
+ Y.use('event-resize', function (Y) {
+ Y.on("windowresize", function() {
+ resize_object();
+ });
+ });
};
/**
* @return bool
*/
function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) {
- global $CFG, $USER, $DB;
+ global $CFG, $USER, $DB, $PAGE;
$usercontext = context_user::instance($user->id, MUST_EXIST);
$systemcontext = context_system::instance();
}
}
- // Preference page. Only visible by administrators.
- if (!$iscurrentuser && is_siteadmin()) {
+ // Preference page.
+ if (!$iscurrentuser && $PAGE->settingsnav->can_view_user_preferences($user->id)) {
$url = new moodle_url('/user/preferences.php', array('userid' => $user->id));
$title = get_string('preferences', 'moodle');
$node = new core_user\output\myprofile\node('administration', 'preferences', $title, null, $url);
public function clear_cache() {
$this->cache->volatile();
}
+
+ /**
+ * Checks to see if there are child nodes available in the specific user's preference node.
+ * If so, then they have the appropriate permissions view this user's preferences.
+ *
+ * @since Moodle 2.9.3
+ * @param int $userid The user's ID.
+ * @return bool True if child nodes exist to view, otherwise false.
+ */
+ public function can_view_user_preferences($userid) {
+ if (is_siteadmin()) {
+ return true;
+ }
+ // See if any nodes are present in the preferences section for this user.
+ $preferencenode = $this->find('userviewingsettings' . $userid, null);
+ if ($preferencenode && $preferencenode->has_children()) {
+ // Run through each child node.
+ foreach ($preferencenode->children as $childnode) {
+ // If the child node has children then this user has access to a link in the preferences page.
+ if ($childnode->has_children()) {
+ return true;
+ }
+ }
+ }
+ // No links found for the user to access on the preferences page.
+ return false;
+ }
}
/**
$jsinit = $this->get_javascript_init_code();
$handlersjs = $this->get_event_handler_code();
- // There is no global Y, make sure it is available in your scope.
- $js = "YUI().use('node', function(Y) {\n{$inyuijs}{$ondomreadyjs}{$jsinit}{$handlersjs}\n});";
+ // There is a global Y, make sure it is available in your scope.
+ $js = "(function() {{$inyuijs}{$ondomreadyjs}{$jsinit}{$handlersjs}})();";
$output .= html_writer::script($js);
$expected = array('user-profile', 'user-profile-*', 'user-*', '*');
$this->assertEquals($expected, array_values(matching_page_type_patterns_from_pattern($pattern)));
}
+
+ public function test_delete_instances() {
+ global $DB;
+ $this->purge_blocks();
+ $this->setAdminUser();
+
+ $regionname = 'a-region';
+ $blockname = $this->get_a_known_block_type();
+ $context = context_system::instance();
+
+ list($page, $blockmanager) = $this->get_a_page_and_block_manager(array($regionname),
+ $context, 'page-type');
+
+ $blockmanager->add_blocks(array($regionname => array($blockname, $blockname, $blockname)), null, null, false, 3);
+ $blockmanager->load_blocks();
+
+ $blocks = $blockmanager->get_blocks_for_region($regionname);
+ $blockids = array();
+ $preferences = array();
+
+ // Create block related data.
+ foreach ($blocks as $block) {
+ $instance = $block->instance;
+ $pref = 'block' . $instance->id . 'hidden';
+ set_user_preference($pref, '123', 123);
+ $preferences[] = $pref;
+ $pref = 'docked_block_instance_' . $instance->id;
+ set_user_preference($pref, '123', 123);
+ $preferences[] = $pref;
+ blocks_set_visibility($instance, $page, 1);
+ $blockids[] = $instance->id;
+ }
+
+ // Confirm what has been set.
+ $this->assertCount(3, $blockids);
+ list($insql, $inparams) = $DB->get_in_or_equal($blockids);
+ $this->assertEquals(3, $DB->count_records_select('block_positions', "blockinstanceid $insql", $inparams));
+ list($insql, $inparams) = $DB->get_in_or_equal($preferences);
+ $this->assertEquals(6, $DB->count_records_select('user_preferences', "name $insql", $inparams));
+
+ // Keep a block on the side.
+ $allblockids = $blockids;
+ $tokeep = array_pop($blockids);
+
+ // Delete and confirm what should have happened.
+ blocks_delete_instances($blockids);
+
+ // Reload the manager.
+ list($page, $blockmanager) = $this->get_a_page_and_block_manager(array($regionname),
+ $context, 'page-type');
+ $blockmanager->load_blocks();
+ $blocks = $blockmanager->get_blocks_for_region($regionname);
+
+ $this->assertCount(1, $blocks);
+ list($insql, $inparams) = $DB->get_in_or_equal($allblockids);
+ $this->assertEquals(1, $DB->count_records_select('block_positions', "blockinstanceid $insql", $inparams));
+ list($insql, $inparams) = $DB->get_in_or_equal($preferences);
+ $this->assertEquals(2, $DB->count_records_select('user_preferences', "name $insql", $inparams));
+
+ $this->assertFalse(context_block::instance($blockids[0], IGNORE_MISSING));
+ $this->assertFalse(context_block::instance($blockids[1], IGNORE_MISSING));
+ context_block::instance($tokeep); // Would throw an exception if it was deleted.
+ }
+
}
/**
--- /dev/null
+----CLIENT----
+Gmail
+----EXPECTEDPLAIN----
+This is a test response
+----EXPECTEDHTML----
+This is a test response
+----FULLSOURCE----
+Delivered-To: nxtmorg+aaaaaaaaaaiaaaaaaaaaagaaaaaaaaahbfpyofgjbkpkwpeh@gmail.com
+Received: by 10.202.174.212 with SMTP id x203csp2773063oie;
+ Wed, 8 Jul 2015 03:45:49 -0700 (PDT)
+X-Received: by 10.194.2.161 with SMTP id 1mr17755340wjv.143.1436352348859;
+ Wed, 08 Jul 2015 03:45:48 -0700 (PDT)
+Return-Path: <dan@moodle.com>
+Received: from mail-wg0-x22d.google.com (mail-wg0-x22d.google.com. [2a00:1450:400c:c00::22d])
+ by mx.google.com with ESMTPS id hf10si1873428wib.2.2015.07.08.03.45.48
+ for <nxtmorg+AAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAhBfpyofgjbKpKWPeH@gmail.com>
+ (version=TLSv1.2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
+ Wed, 08 Jul 2015 03:45:48 -0700 (PDT)
+Received-SPF: pass (google.com: domain of dan@moodle.com designates 2a00:1450:400c:c00::22d as permitted sender) client-ip=2a00:1450:400c:c00::22d;
+Authentication-Results: mx.google.com;
+ spf=pass (google.com: domain of dan@moodle.com designates 2a00:1450:400c:c00::22d as permitted sender) smtp.mail=dan@moodle.com;
+ dkim=pass header.i=@moodle.com;
+ dmarc=pass (p=QUARANTINE dis=NONE) header.from=moodle.com
+Received: by mail-wg0-x22d.google.com with SMTP id x7so186242655wgj.2
+ for <nxtmorg+AAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAhBfpyofgjbKpKWPeH@gmail.com>; Wed, 08 Jul 2015 03:45:48 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+ d=moodle.com; s=google;
+ h=mime-version:references:in-reply-to:from:date:message-id:subject:to
+ :content-type;
+ bh=jBsUlzIsNVo/9X0XRyhQfQKdI8jqA6v/XM5yi08CpW4=;
+ b=oyncjzbEuLnDDSZ4v7AbfMV8rlNClygbSabhxlhdgiUsZEORCGL83ZmjMencwF/MLm
+ a20Eh1Tho/5gGU3ZsacTgV8phNAp0yBl59mzZUVF4wabIQBMbQQlyBJsqn7RbIRky+DA
+ FpneKKLreS29B0BMr+95VGSJ/XRohQZSjw7nY=
+X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+ d=1e100.net; s=20130820;
+ h=x-gm-message-state:mime-version:references:in-reply-to:from:date
+ :message-id:subject:to:content-type;
+ bh=jBsUlzIsNVo/9X0XRyhQfQKdI8jqA6v/XM5yi08CpW4=;
+ b=GXbTXInSb7VhTCe7uIdixAgUNh0tusJkfgc606jk8ZD5xy89IjLcCDaKBY4wPT/xgH
+ KqELVFnwUsAJRBv0ZiflgzvUQ7SC2znVbkfQK8idswgc7p3iaWxXLT/m3HwVrnn0Aord
+ uRlEW1eBraBdOD/as24aCbzBGFPjFDkynfK0dIyCVmXN05p8QE09bYqkOVSh3lDxeZfX
+ AIDjlfC8DmvKZQN68Con86SyzQ6epzs2A3yrQ3oMYxG5yAHFqoXbmQPZLyjWMqx0uJ4L
+ lRnq7wSSLsSA8a9q8RBO8JltmZHa1AShqMkHghh/RISISXyriFezN71F7lt303fDJLvw
+ 5Z9g==
+X-Gm-Message-State: ALoCoQk4VlKEKkaqy6MLYzq2ZN82v3a64TLQZJo0b26DUbmmS8UDpT8tstQh2kodndsV2GgB/bpT
+X-Received: by 10.180.102.74 with SMTP id fm10mr112402988wib.25.1436352348167;
+ Wed, 08 Jul 2015 03:45:48 -0700 (PDT)
+MIME-Version: 1.0
+References: <665442d32a4d85d3ac239d88a146ffdf9becf154c78bf8394bf3bfdbb4c312f6@dan.moodle.local>
+ <b287e7e4769857e7e31675e9b6141ca71b147210ce28f8a4eadd77aef458ddea@dan.moodle.local>
+In-Reply-To: <b287e7e4769857e7e31675e9b6141ca71b147210ce28f8a4eadd77aef458ddea@dan.moodle.local>
+From: Dan Poltawski <dan@moodle.com>
+Date: Wed, 08 Jul 2015 10:45:38 +0000
+Message-ID: <CAOieoNi=VqpcPjhQNwGsw4m-3ATr03CgkriQ4ivgeKQpQtjKqA@mail.gmail.com>
+Subject: Re: Using Moodle Test: Re: A test
+To: nxtmorg+AAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAhBfpyofgjbKpKWPeH@gmail.com
+Content-Type: multipart/alternative; boundary=f46d0444812b7c332b051a5ad7d1
+
+--f46d0444812b7c332b051a5ad7d1
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+This is a test response
+
+On Wed, Jul 8, 2015 at 11:45 AM Admin User <nxtmorg@gmail.com> wrote:
+
+> Using Moodle Test <http://dan.moodle.local/sites/course/view.php?id=3D3>=
+ =C2=BB
+> Forums <http://dan.moodle.local/sites/mod/forum/index.php?id=3D3> =C2=BB =
+Discussion
+> forum <http://dan.moodle.local/sites/mod/forum/view.php?f=3D5> =C2=BB A t=
+est
+> <http://dan.moodle.local/sites/mod/forum/discuss.php?d=3D24>
+> [image: Picture of Admin User]
+> <http://dan.moodle.local/sites/user/view.php?id=3D2&course=3D3>
+> Re: A test
+> by Admin User <http://dan.moodle.local/sites/user/view.php?id=3D2&course=
+=3D3>
+> - Wednesday, 8 July 2015, 11:45 am
+>
+>
+> test 123
+> Show parent
+> <http://dan.moodle.local/sites/mod/forum/discuss.php?d=3D24&parent=3D27> =
+|
+> Reply <http://dan.moodle.local/sites/mod/forum/post.php?reply=3D33>
+> See this post in context
+> <http://dan.moodle.local/sites/mod/forum/discuss.php?d=3D24#p33>
+> ------------------------------
+> Unsubscribe from this forum
+> <http://dan.moodle.local/sites/mod/forum/subscribe.php?id=3D5> Unsubscrib=
+e
+> from this discussion
+> <http://dan.moodle.local/sites/mod/forum/subscribe.php?id=3D5&d=3D24> Uns=
+ubscribe
+> from all forums
+> <http://dan.moodle.local/sites/mod/forum/unsubscribeall.php> Change your
+> forum digest preferences
+> <http://dan.moodle.local/sites/mod/forum/index.php?id=3D3>
+>
+> You can reply to this via email.
+>
+
+--f46d0444812b7c332b051a5ad7d1
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+<div dir=3D"ltr">This is a test response</div><br><div class=3D"gmail_quote=
+"><div dir=3D"ltr">On Wed, Jul 8, 2015 at 11:45 AM Admin User <<a href=
+=3D"mailto:nxtmorg@gmail.com">nxtmorg@gmail.com</a>> wrote:<br></div><bl=
+ockquote class=3D"gmail_quote" style=3D"margin:0 0 0 .8ex;border-left:1px #=
+ccc solid;padding-left:1ex">
+<div>
+
+<div><a href=3D"http://dan.moodle.local/sites/course/view.php?id=3D3" targe=
+t=3D"_blank">Using Moodle Test</a> =C2=BB <a href=3D"http://dan.moodle.loca=
+l/sites/mod/forum/index.php?id=3D3" target=3D"_blank">Forums</a> =C2=BB <a =
+href=3D"http://dan.moodle.local/sites/mod/forum/view.php?f=3D5" target=3D"_=
+blank">Discussion forum</a> =C2=BB <a href=3D"http://dan.moodle.local/sites=
+/mod/forum/discuss.php?d=3D24" target=3D"_blank">A test</a></div><table bor=
+der=3D"0" cellpadding=3D"3" cellspacing=3D"0"><tr><td width=3D"35" valign=
+=3D"top"><a href=3D"http://dan.moodle.local/sites/user/view.php?id=3D2&=
+course=3D3" target=3D"_blank"><img src=3D"http://dan.moodle.local/sites/the=
+me/image.php?theme=3Dmoodleorgcleaned_moodleorg&component=3Dcore&im=
+age=3Du%2Ff2&svg=3D0" alt=3D"Picture of Admin User" title=3D"Picture of=
+ Admin User" width=3D"35" height=3D"35"></a></td><td><div>Re: A test</div><=
+div>by <a href=3D"http://dan.moodle.local/sites/user/view.php?id=3D2&co=
+urse=3D3" target=3D"_blank">Admin User</a> - Wednesday, 8 July 2015, 11:45 =
+am</div></td></tr><tr><td valign=3D"top">=C2=A0</td><td><p>test 123</p><div=
+><a href=3D"http://dan.moodle.local/sites/mod/forum/discuss.php?d=3D24&=
+parent=3D27" target=3D"_blank">Show parent</a> | <a href=3D"http://dan.mood=
+le.local/sites/mod/forum/post.php?reply=3D33" target=3D"_blank">Reply</a></=
+div><div><a href=3D"http://dan.moodle.local/sites/mod/forum/discuss.php?d=
+=3D24#p33" target=3D"_blank">See this post in context</a></div></td></tr></=
+table>
+
+<hr><div><a href=3D"http://dan.moodle.local/sites/mod/forum/subscribe.php?i=
+d=3D5" target=3D"_blank">Unsubscribe from this forum</a>=C2=A0<a href=3D"ht=
+tp://dan.moodle.local/sites/mod/forum/subscribe.php?id=3D5&d=3D24" targ=
+et=3D"_blank">Unsubscribe from this discussion</a>=C2=A0<a href=3D"http://d=
+an.moodle.local/sites/mod/forum/unsubscribeall.php" target=3D"_blank">Unsub=
+scribe from all forums</a>=C2=A0<a href=3D"http://dan.moodle.local/sites/mo=
+d/forum/index.php?id=3D3" target=3D"_blank">Change your forum digest prefer=
+ences</a></div></div><p>You can reply to this via email.</p>
+
+</blockquote></div>
+
+--f46d0444812b7c332b051a5ad7d1--
Havent tried this before and it is awesome....
Cheers
+ Rajesh
----FULLSOURCE----
Delivered-To: moodlehqtest+aaaaaaaaaaiaaaaaaaaabqaaaaaaaaazd63zvl6kcy04ioh+@example.com
return parent::remove_quoted_text($messagedata);
}
- public static function get_linecount_to_remove($messagedata) {
- return parent::get_linecount_to_remove($messagedata);
- }
-
public function get_name() {}
public function get_description() {}
return $node;
}
+ /**
+ * Test that users with the correct permissions can view the preferences page.
+ */
+ public function test_can_view_user_preferences() {
+ global $PAGE, $DB, $SITE;
+ $this->resetAfterTest();
+
+ $persontoview = $this->getDataGenerator()->create_user();
+ $persondoingtheviewing = $this->getDataGenerator()->create_user();
+
+ $PAGE->set_url('/');
+ $PAGE->set_course($SITE);
+
+ // Check that a standard user can not view the preferences page.
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+ $this->getDataGenerator()->role_assign($studentrole->id, $persondoingtheviewing->id);
+ $this->setUser($persondoingtheviewing);
+ $settingsnav = new exposed_settings_navigation();
+ $settingsnav->initialise();
+ $settingsnav->extend_for_user($persontoview->id);
+ $this->assertFalse($settingsnav->can_view_user_preferences($persontoview->id));
+
+ // Set persondoingtheviewing as a manager.
+ $managerrole = $DB->get_record('role', array('shortname' => 'manager'));
+ $this->getDataGenerator()->role_assign($managerrole->id, $persondoingtheviewing->id);
+ $settingsnav = new exposed_settings_navigation();
+ $settingsnav->initialise();
+ $settingsnav->extend_for_user($persontoview->id);
+ $this->assertTrue($settingsnav->can_view_user_preferences($persontoview->id));
+
+ // Check that the admin can view the preferences page.
+ $this->setAdminUser();
+ $settingsnav = new exposed_settings_navigation();
+ $settingsnav->initialise();
+ $settingsnav->extend_for_user($persontoview->id);
+ $preferencenode = $settingsnav->find('userviewingsettings' . $persontoview->id, null);
+ $this->assertTrue($settingsnav->can_view_user_preferences($persontoview->id));
+ }
+
/**
* @depends test_setting__initialise
* @param mixed $node
https://docs.moodle.org/dev/version.php for details (MDL-48494).
* PHPUnit is upgraded to 4.7. Some tests using deprecated assertions etc may need changes to work correctly.
* Users of the text editor API to manually create a text editor should call set_text before calling use_editor.
+* Javascript - SimpleYUI and the Y instance used for modules have been merged. Y is now always the same instance of Y.
* get_referer() has been deprecated, please use the get_local_referer function instead.
* \core\progress\null is renamed to \core\progress\none for improved PHP7 compatibility as null is a reserved word (see MDL-50453).
* \webservice_xmlrpc_client now respects proxy server settings. If your XMLRPC server is available on your local network and not via your proxy server, you may need to add it to the list of proxy
*/
find_element_text: function(n) {
// The valid node types to get text from.
- var nodes = n.all('h2, h3, h4, h5, span, p, div.no-overflow, div.dimmed_text');
+ var nodes = n.all('h2, h3, h4, h5, span:not(.actions):not(.menu-action-text), p, div.no-overflow, div.dimmed_text');
var text = '';
nodes.each(function () {
'requiredentries', 'requiredentriestoview', 'maxentries', 'rssarticles',
'singletemplate', 'listtemplate', 'listtemplateheader', 'listtemplatefooter',
'addtemplate', 'rsstemplate', 'rsstitletemplate', 'csstemplate',
- 'jstemplate', 'asearchtemplate', 'approval', 'scale',
+ 'jstemplate', 'asearchtemplate', 'approval', 'manageapproved', 'scale',
'assessed', 'assesstimestart', 'assesstimefinish', 'defaultsort',
'defaultsortdir', 'editany', 'notification'));
$additionalfields = array('maxentries', 'rssarticles', 'singletemplate', 'listtemplate',
'listtemplateheader', 'listtemplatefooter', 'addtemplate', 'rsstemplate', 'rsstitletemplate',
- 'csstemplate', 'jstemplate', 'asearchtemplate', 'approval', 'scale', 'assessed', 'assesstimestart',
+ 'csstemplate', 'jstemplate', 'asearchtemplate', 'approval', 'manageapproved', 'scale', 'assessed', 'assesstimestart',
'assesstimefinish', 'defaultsort', 'defaultsortdir', 'editany', 'notification');
// This is for avoid a long repetitive list.
'jstemplate' => new external_value(PARAM_RAW, 'jstemplate field', VALUE_OPTIONAL),
'asearchtemplate' => new external_value(PARAM_RAW, 'asearchtemplate field', VALUE_OPTIONAL),
'approval' => new external_value(PARAM_BOOL, 'approval field', VALUE_OPTIONAL),
+ 'manageapproved' => new external_value(PARAM_BOOL, 'manageapproved field', VALUE_OPTIONAL),
'scale' => new external_value(PARAM_INT, 'scale field', VALUE_OPTIONAL),
'assessed' => new external_value(PARAM_INT, 'assessed field', VALUE_OPTIONAL),
'assesstimestart' => new external_value(PARAM_INT, 'assesstimestart field', VALUE_OPTIONAL),
<FIELD NAME="jstemplate" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="asearchtemplate" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="approval" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+ <FIELD NAME="manageapproved" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
<FIELD NAME="scale" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="assessed" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="assesstimestart" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
// Moodle v2.9.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2015092200) {
+
+ // Define field manageapproved to be added to data.
+ $table = new xmldb_table('data');
+ $field = new xmldb_field('manageapproved', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '1', 'approval');
+
+ // Conditionally launch add field manageapproved.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Data savepoint reached.
+ upgrade_mod_savepoint(true, 2015092200, 'data');
+ }
+
return true;
}
if (!has_capability('mod/data:manageentries', $context)) {
if ($rid) {
// User is editing an existing record
- if (!data_isowner($rid) || data_in_readonly_period($data)) {
+ if (!data_user_can_manage_entry($record, $data, $context)) {
print_error('noaccess','data');
}
} else if (!data_user_can_add_entry($data, $currentgroup, $groupmode, $context)) {
return isset($value) && !($value == '');
}
+ /**
+ * Validate values for this field.
+ * Both the Latitude and the Longitude fields need to be filled in.
+ *
+ * @param array $values The entered values for the lat. and long.
+ * @return string|bool Error message or false.
+ */
+ public function field_validation($values) {
+ $valuecount = 0;
+ // The lat long class has two values that need to be checked.
+ foreach ($values as $value) {
+ if (isset($value->value) && !($value->value == '')) {
+ $valuecount++;
+ }
+ }
+ // If we have nothing filled in or both filled in then everything is okay.
+ if ($valuecount == 0 || $valuecount == 2) {
+ return false;
+ }
+ // If we get here then only one field has been filled in.
+ return get_string('latlongboth', 'data');
+ }
}
$string['jstemplate'] = 'Javascript template';
$string['latitude'] = 'Latitude';
$string['latlong'] = 'Latitude/longitude';
+$string['latlongboth'] = 'Both the Latitude and the Longitude must be filled in.';
$string['latlongdownloadallhint'] = 'Download link for all entries as KML';
$string['latlongkmllabelling'] = 'How to label items in KML files (Google Earth)';
$string['latlonglinkservicesdisplayed'] = 'Link-out services to display';
$string['list'] = 'View list';
$string['listtemplate'] = 'List template';
$string['longitude'] = 'Longitude';
+$string['manageapproved'] = 'Allow editing of approved entries';
+$string['manageapproved_help'] = 'If disabled, approved entries are not editable and deletable by its owner. This setting only takes effect if approval required is set to yes. Default is yes.';
$string['mapexistingfield'] = 'Map to {$a}';
$string['mapnewfield'] = 'Create a new field';
$string['mappingwarning'] = 'All old fields not mapped to a new field will be lost and all data in that field will be removed.';
}
$jumpurl = new moodle_url($jumpurl, array('page' => $page, 'sesskey' => sesskey()));
- // Check whether this activity is read-only at present
- $readonly = data_in_readonly_period($data);
-
foreach ($records as $record) { // Might be just one for the single template
// Replacing tags
// Replacing special tags (##Edit##, ##Delete##, ##More##)
$patterns[]='##edit##';
$patterns[]='##delete##';
- if ($canmanageentries || (!$readonly && data_isowner($record->id))) {
+ if (data_user_can_manage_entry($record, $data, $context)) {
$replacement[] = '<a href="'.$CFG->wwwroot.'/mod/data/edit.php?d='
.$data->id.'&rid='.$record->id.'&sesskey='.sesskey().'"><img src="'.$OUTPUT->pix_url('t/edit') . '" class="iconsmall" alt="'.get_string('edit').'" title="'.get_string('edit').'" /></a>';
$replacement[] = '<a href="'.$CFG->wwwroot.'/mod/data/view.php?d='
}
}
+/**
+ * Check whether the current user is allowed to manage the given record considering manageentries capability,
+ * data_in_readonly_period() result, ownership (determined by data_isowner()) and manageapproved setting.
+ * @param mixed $record record object or id
+ * @param object $data data object
+ * @param object $context context object
+ * @return bool returns true if the user is allowd to edit the entry, false otherwise
+ */
+function data_user_can_manage_entry($record, $data, $context) {
+ global $DB;
+
+ if (has_capability('mod/data:manageentries', $context)) {
+ return true;
+ }
+
+ // Check whether this activity is read-only at present.
+ $readonly = data_in_readonly_period($data);
+
+ if (!$readonly) {
+ // Get record object from db if just id given like in data_isowner.
+ // ...done before calling data_isowner() to avoid querying db twice.
+ if (!is_object($record)) {
+ if (!$record = $DB->get_record('data_records', array('id' => $record))) {
+ return false;
+ }
+ }
+ if (data_isowner($record)) {
+ if ($data->approval && $record->approved) {
+ return $data->manageapproved == 1;
+ } else {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
/**
* Check whether the specified database activity is currently in a read-only period
*
'maxentries',
'rssarticles',
'approval',
+ 'manageapproved',
'defaultsortdir'
);
// Empty form checking - you can't submit an empty form.
$emptyform = true;
$requiredfieldsfilled = true;
+ $fieldsvalidated = true;
// Store the notifications.
$result->generalnotifications = array();
$field = data_get_field($fieldrecord, $mod);
if (isset($submitteddata[$fieldrecord->id])) {
+ // Field validation check.
+ if (method_exists($field, 'field_validation')) {
+ $errormessage = $field->field_validation($submitteddata[$fieldrecord->id]);
+ if ($errormessage) {
+ $result->fieldnotifications[$field->field->name][] = $errormessage;
+ $fieldsvalidated = false;
+ }
+ }
foreach ($submitteddata[$fieldrecord->id] as $fieldname => $value) {
if ($field->notemptyfield($value->value, $value->fieldname)) {
// The field has content and the form is not empty.
$result->generalnotifications[] = get_string('emptyaddform', 'data');
}
- $result->validated = $requiredfieldsfilled && !$emptyform;
+ $result->validated = $requiredfieldsfilled && !$emptyform && $fieldsvalidated;
return $result;
}
$mform->addElement('selectyesno', 'approval', get_string('requireapproval', 'data'));
$mform->addHelpButton('approval', 'requireapproval', 'data');
+ $mform->addElement('selectyesno', 'manageapproved', get_string('manageapproved', 'data'));
+ $mform->addHelpButton('manageapproved', 'manageapproved', 'data');
+ $mform->setDefault('manageapproved', 1);
+ $mform->disabledIf('manageapproved', 'approval', 'eq', 0);
+
$mform->addElement('selectyesno', 'comments', get_string('allowcomments', 'data'));
$countoptions = array(0=>get_string('none'))+
--- /dev/null
+@mod @mod_data
+Feature: Users can edit approved entries in database activities
+ In order to control whether approved database entries can be changed
+ As a teacher
+ I need to be able to enable or disable management of approved entries
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | 1 | student1@example.com |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+
+ @javascript
+ Scenario: Students can manage their approved entries to a database
+ # Create database activity and allow editing of
+ # approved entries.
+ And I add a "Database" to section "1" and I fill the form with:
+ | Name | Test database name |
+ | Description | Test |
+ | id_approval | Yes |
+ | id_manageapproved | Yes |
+ And I add a "Text input" field to "Test database name" database and I fill the form with:
+ | Field name | Test field name |
+ | Field description | Test field description |
+ # To generate the default templates.
+ And I follow "Templates"
+ And I log out
+ # Add an entry as a student.
+ And I log in as "student1"
+ And I follow "Course 1"
+ And I add an entry to "Test database name" database with:
+ | Test field name | Student entry |
+ And I press "Save and view"
+ And I log out
+ # Approve the student's entry as a teacher.
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I follow "Test database name"
+ And I follow "Approve"
+ And I log out
+ # Make sure the student can still edit their entry after it's approved.
+ When I log in as "student1"
+ And I follow "Course 1"
+ And I follow "Test database name"
+ Then I should see "Student entry"
+ And "Edit" "link" should exist
+
+ @javascript
+ Scenario: Students can not manage their approved entries to a database
+ # Create database activity and don't allow editing of
+ # approved entries.
+ And I add a "Database" to section "1" and I fill the form with:
+ | Name | Test database name |
+ | Description | Test |
+ | id_approval | Yes |
+ | id_manageapproved | No |
+ And I add a "Text input" field to "Test database name" database and I fill the form with:
+ | Field name | Test field name |
+ | Field description | Test field description |
+ # To generate the default templates.
+ And I follow "Templates"
+ And I log out
+ # Add an entry as a student.
+ And I log in as "student1"
+ And I follow "Course 1"
+ And I add an entry to "Test database name" database with:
+ | Test field name | Student entry |
+ And I press "Save and view"
+ And I log out
+ # Approve the student's entry as a teacher.
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I follow "Test database name"
+ And I follow "Approve"
+ And I log out
+ # Make sure the student isn't able to edit their entry after it's approved.
+ When I log in as "student1"
+ And I follow "Course 1"
+ And I follow "Test database name"
+ Then I should see "Student entry"
+ And "Edit" "link" should not exist
| Required URL | http://example.com/ |
| Required Multimenu | Option 1 |
| Required Two-Option Multimenu | Option 1 |
+
+ Scenario: A student fills in Latitude but not Longitude will see an error
+ Given I log in as "student1"
+ And I follow "Course 1"
+ When I add an entry to "Test database name" database with:
+ | Base Text input | Some input to allow us to submit the otherwise empty form |
+ | Required Checkbox Option 1 | 1 |
+ | RTOC Option 1 | 1 |
+ | Latitude | 24 |
+ | Required Menu | 1 |
+ | Required Number | 1 |
+ | Required Radio Option 1 | 1 |
+ | Required Text input | New entry text |
+ | Required Text area | More text |
+ | Required URL | http://example.com/ |
+ | Required Multimenu | 1 |
+ | Required Two-Option Multimenu | 1 |
+ And I set the field with xpath "//div[@title='Not required Latlong']//tr[td/label[normalize-space(.)='Latitude']]/td/input" to "20"
+ And I press "Save and view"
+ Then ".alert.alert-error" "css_element" should exist in the "Required Latlong" "table_row"
+ And ".alert.alert-error" "css_element" should exist in the "Not required Latlong" "table_row"
$additionalfields = array('maxentries', 'rssarticles', 'singletemplate', 'listtemplate',
'listtemplateheader', 'listtemplatefooter', 'addtemplate', 'rsstemplate', 'rsstitletemplate',
'csstemplate', 'jstemplate', 'asearchtemplate', 'approval', 'scale', 'assessed', 'assesstimestart',
- 'assesstimefinish', 'defaultsort', 'defaultsortdir', 'editany', 'notification');
+ 'assesstimefinish', 'defaultsort', 'defaultsortdir', 'editany', 'notification', 'manageapproved');
foreach ($additionalfields as $field) {
if ($field == 'approval' or $field == 'editany') {
$this->assertEventContextNotUsed($event);
}
+ /**
+ * Checks that data_user_can_manage_entry will return true if the user
+ * has the mod/data:manageentries capability.
+ */
+ public function test_data_user_can_manage_entry_return_true_with_capability() {
+
+ $this->resetAfterTest();
+ $testdata = $this->create_user_test_data();
+
+ $user = $testdata['user'];
+ $course = $testdata['course'];
+ $roleid = $testdata['roleid'];
+ $context = $testdata['context'];
+ $record = $testdata['record'];
+ $data = new stdClass();
+
+ $this->setUser($user);
+
+ assign_capability('mod/data:manageentries', CAP_ALLOW, $roleid, $context);
+
+ $this->assertTrue(data_user_can_manage_entry($record, $data, $context),
+ 'data_user_can_manage_entry() returns true if the user has mod/data:manageentries capability');
+ }
+
+ /**
+ * Checks that data_user_can_manage_entry will return false if the data
+ * is set to readonly.
+ */
+ public function test_data_user_can_manage_entry_return_false_readonly() {
+
+ $this->resetAfterTest();
+ $testdata = $this->create_user_test_data();
+
+ $user = $testdata['user'];
+ $course = $testdata['course'];
+ $roleid = $testdata['roleid'];
+ $context = $testdata['context'];
+ $record = $testdata['record'];
+ $data = new stdClass();
+ // Causes readonly mode to be enable.
+ $now = time();
+ $data->timeviewfrom = $now;
+ $data->timeviewto = $now;
+
+ $this->setUser($user);
+
+ // Need to make sure they don't have this capability in order to fall back to
+ // the other checks.
+ assign_capability('mod/data:manageentries', CAP_PROHIBIT, $roleid, $context);
+
+ $this->assertFalse(data_user_can_manage_entry($record, $data, $context),
+ 'data_user_can_manage_entry() returns false if the data is read only');
+ }
+
+ /**
+ * Checks that data_user_can_manage_entry will return false if the record
+ * can't be found in the database.
+ */
+ public function test_data_user_can_manage_entry_return_false_no_record() {
+
+ $this->resetAfterTest();
+ $testdata = $this->create_user_test_data();
+
+ $user = $testdata['user'];
+ $course = $testdata['course'];
+ $roleid = $testdata['roleid'];
+ $context = $testdata['context'];
+ $record = $testdata['record'];
+ $data = new stdClass();
+ // Causes readonly mode to be disabled.
+ $now = time();
+ $data->timeviewfrom = $now + 100;
+ $data->timeviewto = $now - 100;
+
+ $this->setUser($user);
+
+ // Need to make sure they don't have this capability in order to fall back to
+ // the other checks.
+ assign_capability('mod/data:manageentries', CAP_PROHIBIT, $roleid, $context);
+
+ // Pass record id instead of object to force DB lookup.
+ $this->assertFalse(data_user_can_manage_entry(1, $data, $context),
+ 'data_user_can_manage_entry() returns false if the record cannot be found');
+ }
+
+ /**
+ * Checks that data_user_can_manage_entry will return false if the record
+ * isn't owned by the user.
+ */
+ public function test_data_user_can_manage_entry_return_false_not_owned_record() {
+
+ $this->resetAfterTest();
+ $testdata = $this->create_user_test_data();
+
+ $user = $testdata['user'];
+ $course = $testdata['course'];
+ $roleid = $testdata['roleid'];
+ $context = $testdata['context'];
+ $record = $testdata['record'];
+ $data = new stdClass();
+ // Causes readonly mode to be disabled.
+ $now = time();
+ $data->timeviewfrom = $now + 100;
+ $data->timeviewto = $now - 100;
+ // Make sure the record isn't owned by this user.
+ $record->userid = $user->id + 1;
+
+ $this->setUser($user);
+
+ // Need to make sure they don't have this capability in order to fall back to
+ // the other checks.
+ assign_capability('mod/data:manageentries', CAP_PROHIBIT, $roleid, $context);
+
+ $this->assertFalse(data_user_can_manage_entry($record, $data, $context),
+ 'data_user_can_manage_entry() returns false if the record isnt owned by the user');
+ }
+
+ /**
+ * Checks that data_user_can_manage_entry will return true if the data
+ * doesn't require approval.
+ */
+ public function test_data_user_can_manage_entry_return_true_data_no_approval() {
+
+ $this->resetAfterTest();
+ $testdata = $this->create_user_test_data();
+
+ $user = $testdata['user'];
+ $course = $testdata['course'];
+ $roleid = $testdata['roleid'];
+ $context = $testdata['context'];
+ $record = $testdata['record'];
+ $data = new stdClass();
+ // Causes readonly mode to be disabled.
+ $now = time();
+ $data->timeviewfrom = $now + 100;
+ $data->timeviewto = $now - 100;
+ // The record doesn't need approval.
+ $data->approval = false;
+ // Make sure the record is owned by this user.
+ $record->userid = $user->id;
+
+ $this->setUser($user);
+
+ // Need to make sure they don't have this capability in order to fall back to
+ // the other checks.
+ assign_capability('mod/data:manageentries', CAP_PROHIBIT, $roleid, $context);
+
+ $this->assertTrue(data_user_can_manage_entry($record, $data, $context),
+ 'data_user_can_manage_entry() returns true if the record doesnt require approval');
+ }
+
+ /**
+ * Checks that data_user_can_manage_entry will return true if the record
+ * isn't yet approved.
+ */
+ public function test_data_user_can_manage_entry_return_true_record_unapproved() {
+
+ $this->resetAfterTest();
+ $testdata = $this->create_user_test_data();
+
+ $user = $testdata['user'];
+ $course = $testdata['course'];
+ $roleid = $testdata['roleid'];
+ $context = $testdata['context'];
+ $record = $testdata['record'];
+ $data = new stdClass();
+ // Causes readonly mode to be disabled.
+ $now = time();
+ $data->timeviewfrom = $now + 100;
+ $data->timeviewto = $now - 100;
+ // The record needs approval.
+ $data->approval = true;
+ // Make sure the record is owned by this user.
+ $record->userid = $user->id;
+ // The record hasn't yet been approved.
+ $record->approved = false;
+
+ $this->setUser($user);
+
+ // Need to make sure they don't have this capability in order to fall back to
+ // the other checks.
+ assign_capability('mod/data:manageentries', CAP_PROHIBIT, $roleid, $context);
+
+ $this->assertTrue(data_user_can_manage_entry($record, $data, $context),
+ 'data_user_can_manage_entry() returns true if the record is not yet approved');
+ }
+
+ /**
+ * Checks that data_user_can_manage_entry will return the 'manageapproved'
+ * value if the record has already been approved.
+ */
+ public function test_data_user_can_manage_entry_return_manageapproved() {
+
+ $this->resetAfterTest();
+ $testdata = $this->create_user_test_data();
+
+ $user = $testdata['user'];
+ $course = $testdata['course'];
+ $roleid = $testdata['roleid'];
+ $context = $testdata['context'];
+ $record = $testdata['record'];
+ $data = new stdClass();
+ // Causes readonly mode to be disabled.
+ $now = time();
+ $data->timeviewfrom = $now + 100;
+ $data->timeviewto = $now - 100;
+ // The record needs approval.
+ $data->approval = true;
+ // Can the user managed approved records?
+ $data->manageapproved = false;
+ // Make sure the record is owned by this user.
+ $record->userid = $user->id;
+ // The record has been approved.
+ $record->approved = true;
+
+ $this->setUser($user);
+
+ // Need to make sure they don't have this capability in order to fall back to
+ // the other checks.
+ assign_capability('mod/data:manageentries', CAP_PROHIBIT, $roleid, $context);
+
+ $canmanageentry = data_user_can_manage_entry($record, $data, $context);
+
+ // Make sure the result of the check is what ever the manageapproved setting
+ // is set to.
+ $this->assertEquals($data->manageapproved, $canmanageentry,
+ 'data_user_can_manage_entry() returns the manageapproved setting on approved records');
+ }
+
+ /**
+ * Helper method to create a set of test data for data_user_can_manage tests
+ *
+ * @return array contains user, course, roleid, module, context and record
+ */
+ private function create_user_test_data() {
+ $user = $this->getDataGenerator()->create_user();
+ $course = $this->getDataGenerator()->create_course();
+ $roleid = $this->getDataGenerator()->create_role();
+ $record = new stdClass();
+ $record->name = "test name";
+ $record->intro = "test intro";
+ $record->comments = 1;
+ $record->course = $course->id;
+ $record->userid = $user->id;
+
+ $module = $this->getDataGenerator()->create_module('data', $record);
+ $cm = get_coursemodule_from_instance('data', $module->id, $course->id);
+ $context = context_module::instance($module->cmid);
+
+ $this->getDataGenerator()->role_assign($roleid, $user->id, $context->id);
+
+ return array(
+ 'user' => $user,
+ 'course' => $course,
+ 'roleid' => $roleid,
+ 'module' => $module,
+ 'context' => $context,
+ 'record' => $record
+ );
+ }
+
/**
* Tests for mod_data_rating_can_see_item_ratings().
*
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015051100; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2015092200; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2015050500; // Requires this Moodle version
$plugin->component = 'mod_data'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
/// Delete any requested records
- if ($delete && confirm_sesskey() && ($canmanageentries or data_isowner($delete))) {
+ if ($delete && confirm_sesskey() && (data_user_can_manage_entry($delete, $data, $context))) {
if ($confirm = optional_param('confirm',0,PARAM_INT)) {
if (data_delete_record($delete, $data, $course->id, $cm->id)) {
echo $OUTPUT->notification(get_string('recorddeleted','data'), 'notifysuccess');
$params = self::validate_parameters(self::get_forums_by_courses_parameters(), array('courseids' => $courseids));
if (empty($params['courseids'])) {
- // Get all the courses the user can view.
- $courseids = array_keys(enrol_get_my_courses());
- } else {
- $courseids = $params['courseids'];
+ $params['courseids'] = array_keys(enrol_get_my_courses());
}
// Array to store the forums to return.
$arrforums = array();
+ $warnings = array();
// Ensure there are courseids to loop through.
- if (!empty($courseids)) {
- // Array of the courses we are going to retrieve the forums from.
- $dbcourses = array();
- // Mod info for courses.
- $modinfocourses = array();
-
- // Go through the courseids and return the forums.
- foreach ($courseids as $courseid) {
- // Check the user can function in this context.
- try {
- $context = context_course::instance($courseid);
- self::validate_context($context);
- // Get the modinfo for the course.
- $modinfocourses[$courseid] = get_fast_modinfo($courseid);
- $dbcourses[$courseid] = $modinfocourses[$courseid]->get_course();
-
- } catch (Exception $e) {
- continue;
- }
- }
+ if (!empty($params['courseids'])) {
+
+ list($courses, $warnings) = external_util::validate_courses($params['courseids']);
// Get the forums in this course. This function checks users visibility permissions.
- if ($forums = get_all_instances_in_courses("forum", $dbcourses)) {
- foreach ($forums as $forum) {
+ $forums = get_all_instances_in_courses("forum", $courses);
+ foreach ($forums as $forum) {
- $course = $dbcourses[$forum->course];
- $cm = $modinfocourses[$course->id]->get_cm($forum->coursemodule);
- $context = context_module::instance($cm->id);
+ $course = $courses[$forum->course];
+ $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id);
+ $context = context_module::instance($cm->id);
- // Skip forums we are not allowed to see discussions.
- if (!has_capability('mod/forum:viewdiscussion', $context)) {
- continue;
- }
+ // Skip forums we are not allowed to see discussions.
+ if (!has_capability('mod/forum:viewdiscussion', $context)) {
+ continue;
+ }
- // Format the intro before being returning using the format setting.
- list($forum->intro, $forum->introformat) = external_format_text($forum->intro, $forum->introformat,
- $context->id, 'mod_forum', 'intro', 0);
- // Discussions count. This function does static request cache.
- $forum->numdiscussions = forum_count_discussions($forum, $cm, $course);
- $forum->cmid = $forum->coursemodule;
+ // Format the intro before being returning using the format setting.
+ list($forum->intro, $forum->introformat) = external_format_text($forum->intro, $forum->introformat,
+ $context->id, 'mod_forum', 'intro', 0);
+ // Discussions count. This function does static request cache.
+ $forum->numdiscussions = forum_count_discussions($forum, $cm, $course);
+ $forum->cmid = $forum->coursemodule;
+ $forum->cancreatediscussions = forum_user_can_post_discussion($forum, null, -1, $cm, $context);
- // Add the forum to the array to return.
- $arrforums[$forum->id] = $forum;
- }
+ // Add the forum to the array to return.
+ $arrforums[$forum->id] = $forum;
}
}
'completionreplies' => new external_value(PARAM_INT, 'Student must post replies'),
'completionposts' => new external_value(PARAM_INT, 'Student must post discussions or replies'),
'cmid' => new external_value(PARAM_INT, 'Course module id'),
- 'numdiscussions' => new external_value(PARAM_INT, 'Number of discussions in the forum', VALUE_OPTIONAL)
+ 'numdiscussions' => new external_value(PARAM_INT, 'Number of discussions in the forum', VALUE_OPTIONAL),
+ 'cancreatediscussions' => new external_value(PARAM_BOOL, 'If the user can create discussions', VALUE_OPTIONAL),
), 'forum'
)
);
$discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
// Expect one discussion.
$forum1->numdiscussions = 1;
+ $forum1->cancreatediscussions = true;
$record = new stdClass();
$record->course = $course2->id;
$discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
// Expect two discussions.
$forum2->numdiscussions = 2;
+ // Default limited role, no create discussion capability enabled.
+ $forum2->cancreatediscussions = false;
// Check the forum was correctly created.
$this->assertEquals(2, $DB->count_records_select('forum', 'id = :forum1 OR id = :forum2',
$forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
$this->assertCount(1, $forums);
$this->assertEquals($expectedforums[$forum1->id], $forums[0]);
+ $this->assertTrue($forums[0]['cancreatediscussions']);
+
+ // Change the type of the forum, the user shouldn't be able to add discussions.
+ $DB->set_field('forum', 'type', 'news', array('id' => $forum1->id));
+ $forums = mod_forum_external::get_forums_by_courses();
+ $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
+ $this->assertFalse($forums[0]['cancreatediscussions']);
// Call for the second course we unenrolled the user from.
$forums = mod_forum_external::get_forums_by_courses(array($course2->id));
This files describes API changes in /mod/forum/*,
information provided here is intended especially for developers.
+=== 3.0 ===
+ * External function get_forums_by_courses now returns and additional field "cancreatediscussions" that indicates if the user
+ can create discussions in the forum.
+
=== 2.8 ===
* The following functions have all been marked as deprecated. Many of
these have not been supported in many releases and should not be relied
$mform->closeHeaderBefore('buttonar');
}
+ /**
+ * Validate the submitted form data.
+ *
+ * Grading strategy plugins can provide their own validation rules by
+ * overriding the {@link self::validation_inner()} method.
+ *
+ * @param array $data
+ * @param array $files
+ * @return array
+ */
+ final public function validation($data, $files) {
+ return array_merge(
+ parent::validation($data, $files),
+ $this->validation_inner($data, $files)
+ );
+ }
+
/**
* Add any strategy specific form fields.
*
// By default, do nothing.
}
+ /**
+ * Add strategy specific validation rules.
+ *
+ * @param array $data
+ * @param array $files
+ * @return array
+ */
+ protected function validation_inner($data, $files) {
+ return array();
+ }
}
$mform->setDefault('config_layout', 'list');
$this->set_data($current);
}
+
+ /**
+ * Provide validation rules for the rubric editor form.
+ *
+ * @param array $data
+ * @param array $files
+ * @return array
+ */
+ protected function validation_inner($data, $files) {
+
+ $errors = array();
+
+ // Iterate over all submitted dimensions (criteria).
+ for ($i = 0; isset($data['dimensionid__idx_'.$i]); $i++) {
+
+ $dimgrades = array();
+
+ if (0 == strlen(trim($data['description__idx_'.$i.'_editor']['text']))) {
+ // The description text is empty and this criterion will be deleted.
+ continue;
+ }
+
+ // Make sure the levels grades are unique within the criterion.
+ for ($j = 0; isset($data['levelid__idx_'.$i.'__idy_'.$j]); $j++) {
+ if (0 == strlen(trim($data['definition__idx_'.$i.'__idy_'.$j]))) {
+ // The level definition is empty and will not be saved.
+ continue;
+ }
+
+ $levelgrade = $data['grade__idx_'.$i.'__idy_'.$j];
+
+ if (isset($dimgrades[$levelgrade])) {
+ // This grade has already been set for another level.
+ $k = $dimgrades[$levelgrade];
+ $errors['level__idx_'.$i.'__idy_'.$j] = $errors['level__idx_'.$i.'__idy_'.$k] = get_string('mustbeunique',
+ 'workshopform_rubric');
+ } else {
+ $dimgrades[$levelgrade] = $j;
+ }
+ }
+ }
+
+ return $errors;
+ }
}
$string['layoutlist'] = 'List';
$string['levelgroup'] = 'Level grade and definition';
$string['levels'] = 'Levels';
+$string['mustbeunique'] = 'Level grades must be unique within a criterion';
$string['mustchooseone'] = 'You have to select one of these items';
$string['pluginname'] = 'Rubric';
require_once($CFG->dirroot . '/my/lib.php');
require_once($CFG->libdir.'/adminlib.php');
-$edit = optional_param('edit', null, PARAM_BOOL); // Turn editing on and off
+$resetall = optional_param('resetall', null, PARAM_BOOL);
require_login();
$PAGE->set_blocks_editing_capability('moodle/my:configsyspages');
admin_externalpage_setup('mypage', '', null, '', array('pagelayout' => 'mydashboard'));
+if ($resetall && confirm_sesskey()) {
+ my_reset_page_for_all_users(MY_PAGE_PRIVATE, 'my-index');
+ redirect($PAGE->url, get_string('alldashboardswerereset', 'my'));
+}
+
// Override pagetype to show blocks properly.
$PAGE->set_pagetype('my-index');
}
$PAGE->set_subpage($currentpage->id);
+// Display a button to reset everyone's dashboard.
+$url = new moodle_url($PAGE->url, array('resetall' => 1));
+$button = $OUTPUT->single_button($url, get_string('reseteveryonesdashboard', 'my'));
+$PAGE->set_button($button . $PAGE->button);
+
echo $OUTPUT->header();
echo $OUTPUT->custom_block_region('content');
return $systempage;
}
+/**
+ * Resets the page customisations for all users.
+ *
+ * @param int $private Either MY_PAGE_PRIVATE or MY_PAGE_PUBLIC.
+ * @param string $pagetype Either my-index or user-profile.
+ * @return void
+ */
+function my_reset_page_for_all_users($private = MY_PAGE_PRIVATE, $pagetype = 'my-index') {
+ global $DB;
+
+ // Find all the user pages.
+ $where = 'userid IS NOT NULL AND private = :private';
+ $params = array('private' => $private);
+ $pages = $DB->get_recordset_select('my_pages', $where, $params, 'id, userid');
+ $pageids = array();
+ $blockids = array();
+
+ foreach ($pages as $page) {
+ $pageids[] = $page->id;
+ $usercontext = context_user::instance($page->userid);
+
+ // Find all block instances in that page.
+ $blocks = $DB->get_recordset('block_instances', array('parentcontextid' => $usercontext->id,
+ 'pagetypepattern' => $pagetype), '', 'id, subpagepattern');
+ foreach ($blocks as $block) {
+ if (is_null($block->subpagepattern) || $block->subpagepattern == $page->id) {
+ $blockids[] = $block->id;
+ }
+ }
+ $blocks->close();
+ }
+ $pages->close();
+
+ // Wrap the SQL queries in a transaction.
+ $transaction = $DB->start_delegated_transaction();
+
+ // Delete the block instances.
+ if (!empty($blockids)) {
+ blocks_delete_instances($blockids);
+ }
+
+ // Finally delete the pages.
+ if (!empty($pageids)) {
+ list($insql, $inparams) = $DB->get_in_or_equal($pageids);
+ $DB->delete_records_select('my_pages', "id $insql", $pageids);
+ }
+
+ // We should be good to go now.
+ $transaction->allow_commit();
+}
+
class my_syspage_block_manager extends block_manager {
// HACK WARNING!
// TODO: figure out a better way to do this
--- /dev/null
+@core @core_my
+Feature: Reset all personalised pages to default
+ In order to reset everyone's personalised pages
+ As an admin
+ I need to press a button on the pages to customise the default pages
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | 1 | student1@example.com |
+ | student2 | Student | 2 | student2@example.com |
+ | student3 | Student | 3 | student3@example.com |
+ And I log in as "admin"
+ And I set the following system permissions of "Authenticated user" role:
+ | block/myprofile:addinstance | Allow |
+ | moodle/block:edit | Allow |
+ And I log out
+
+ And I log in as "student1"
+ And I follow "Dashboard" in the user menu
+ And I press "Customise this page"
+ And I add the "Comments" block
+ And I press "Stop customising this page"
+ And I should see "Comments"
+ And I log out
+
+ And I log in as "student2"
+ And I follow "Profile" in the user menu
+ And I should not see "Logged in user"
+ And I press "Customise this page"
+ And I add the "Logged in user" block
+ And I press "Stop customising this page"
+ And I should see "Logged in user"
+ And I log out
+
+ And I log in as "student3"
+ And I follow "Dashboard" in the user menu
+ And I should not see "Comments"
+ And I follow "Profile" in the user menu
+ And I should not see "Logged in user"
+ And I log out
+
+ Scenario: Reset Dashboard for all users
+ Given I log in as "admin"
+ And I navigate to "Default Dashboard page" node in "Site administration > Appearance"
+ And I press "Blocks editing on"
+ And I add the "Latest news" block
+ And I open the "Online users" blocks action menu
+ And I follow "Delete Online users"
+ And I press "Yes"
+ And I press "Blocks editing off"
+ And I log out
+
+ And I log in as "student1"
+ And I follow "Dashboard" in the user menu
+ And I should not see "Latest news"
+ And I should see "Online users"
+ And I log out
+
+ And I log in as "student3"
+ And I follow "Dashboard" in the user menu
+ And I should not see "Latest news"
+ And I should see "Online users"
+ And I log out
+
+ And I log in as "admin"
+ And I navigate to "Default Dashboard page" node in "Site administration > Appearance"
+ When I press "Reset Dashboard for all users"
+ And I follow "Continue"
+ And I log out
+
+ And I log in as "student1"
+ And I follow "Dashboard" in the user menu
+ Then I should see "Latest news"
+ And I should not see "Comments"
+ And I should not see "Online users"
+ And I log out
+
+ And I log in as "student3"
+ And I follow "Dashboard" in the user menu
+ And I should see "Latest news"
+ And I should not see "Online users"
+ And I log out
+
+ # Check that this did not affect the customised profiles.
+ And I log in as "student2"
+ And I follow "Profile" in the user menu
+ And I should see "Logged in user"
+ And I should not see "Latest news"
+
+ Scenario: Reset profile for all users
+ Given I log in as "admin"
+ And I navigate to "Default profile page" node in "Site administration > Appearance"
+ And I press "Blocks editing on"
+ And I add the "Latest news" block
+ And I log out
+
+ And I log in as "student2"
+ And I follow "Profile" in the user menu
+ And I should not see "Latest news"
+ And I log out
+
+ And I log in as "student3"
+ And I follow "Profile" in the user menu
+ And I should not see "Latest news"
+ And I log out
+
+ And I log in as "admin"
+ And I navigate to "Default profile page" node in "Site administration > Appearance"
+ When I press "Reset profile for all users"
+ And I follow "Continue"
+ And I log out
+
+ And I log in as "student2"
+ And I follow "Profile" in the user menu
+ Then I should see "Latest news"
+ And I should not see "Logged in user"
+ And I log out
+
+ And I log in as "student3"
+ And I follow "Profile" in the user menu
+ And I should see "Latest news"
+ And I log out
+
+ # Check that this did not affect the customised dashboards.
+ And I log in as "student1"
+ And I follow "Dashboard" in the user menu
+ And I should see "Comments"
+ And I should not see "Latest news"
$deleteurl = new \moodle_url($baseurl, array('deleteselected' => $questionlist, 'confirm' => md5($questionlist),
'sesskey' => sesskey()));
- echo $OUTPUT->confirm(get_string('deletequestionscheck', 'question', $questionnames), $deleteurl, $baseurl);
+ $continue = new \single_button($deleteurl, get_string('delete'), 'post');
+ echo $OUTPUT->confirm(get_string('deletequestionscheck', 'question', $questionnames), $continue, $baseurl);
return true;
}
And I click on "Edit tag name" "link" in the "Cat" "table_row"
And I set the field "New name for tag Cat" to "Kitten"
And I press key "13" in the field "New name for tag Cat"
- # TODO MDL-51311 : replace with "And I wait until the page is ready".
- And I wait "2" seconds
Then I should not see "Cat"
And "New name for tag" "field" should not be visible
And I wait until "Kitten" "link" exists
$userid = optional_param('userid', $USER->id, PARAM_INT);
$currentuser = $userid == $USER->id;
-// Only administrators can access another user's preferences.
-if (!$currentuser && !is_siteadmin($USER)) {
- throw new moodle_exception('cannotedituserpreferences', 'error');
-}
-
// Check that the user is a valid user.
$user = core_user::get_user($userid);
if (!$user || !core_user::is_real_user($userid)) {
if (!$currentuser) {
$PAGE->navigation->extend_for_user($user);
- $settings = $PAGE->settingsnav->find('userviewingsettings' . $user->id, null);
- $settings->make_active();
+ // Need to check that settings exist.
+ if ($settings = $PAGE->settingsnav->find('userviewingsettings' . $user->id, null)) {
+ $settings->make_active();
+ }
$url = new moodle_url('/user/preferences.php', array('userid' => $userid));
$navbar = $PAGE->navbar->add(get_string('preferences', 'moodle'), $url);
+ // Show an error if there are no preferences that this user has access to.
+ if (!$PAGE->settingsnav->can_view_user_preferences($userid)) {
+ throw new moodle_exception('cannotedituserpreferences', 'error');
+ }
} else {
// Shutdown the users node in the navigation menu.
$usernode = $PAGE->navigation->find('users', null);
require_once($CFG->dirroot . '/my/lib.php');
require_once($CFG->libdir.'/adminlib.php');
-$edit = optional_param('edit', null, PARAM_BOOL); // Turn editing on and off.
+$resetall = optional_param('resetall', null, PARAM_BOOL);
require_login();
$PAGE->set_blocks_editing_capability('moodle/my:configsyspages');
admin_externalpage_setup('profilepage', '', null, '', array('pagelayout' => 'mypublic'));
+if ($resetall && confirm_sesskey()) {
+ my_reset_page_for_all_users(MY_PAGE_PUBLIC, 'user-profile');
+ redirect($PAGE->url, get_string('allprofileswerereset', 'my'));
+}
+
// Override pagetype to show blocks properly.
$PAGE->set_pagetype('user-profile');
}
$PAGE->set_subpage($currentpage->id);
+$url = new moodle_url($PAGE->url, array('resetall' => 1));
+$button = $OUTPUT->single_button($url, get_string('reseteveryonesprofile', 'my'));
+$PAGE->set_button($button . $PAGE->button);
echo $OUTPUT->header();
--- /dev/null
+@core @core_user
+Feature: Access to preferences page
+ In order to view the preferences page
+ As a user
+ I need global permissions to view the page.
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | 1 | student1@example.com |
+ | student2 | Student | 2 | student2@example.com |
+ | manager1 | Manager | 1 | manager1@example.com |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | parent | Parent | 1 | parent1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | format |
+ | Course 1 | C1 | topics |
+ | Course 2 | C2 | topics |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | student1 | C1 | student |
+ | student2 | C1 | student |
+ | teacher1 | C1 | editingteacher |
+ And the following "system role assigns" exist:
+ | user | course | role |
+ | manager1 | Acceptance test site | manager |
+
+ Scenario: A student and teacher with normal permissions can not view another user's permissions page.
+ Given I log in as "student1"
+ And I follow "Course 1"
+ And I navigate to "Participants" node in "Current course > C1"
+ And I follow "Student 2"
+ And I should not see "Preferences" in the "#region-main" "css_element"
+ And I log out
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ When I navigate to "Participants" node in "Current course > C1"
+ And I follow "Student 2"
+ Then I should not see "Preferences" in the "#region-main" "css_element"
+
+ Scenario: Administrators and Managers can view another user's permissions page.
+ Given I log in as "admin"
+ And I am on site homepage
+ And I follow "Course 1"
+ And I navigate to "Participants" node in "Current course > C1"
+ And I follow "Student 2"
+ And I should see "Preferences" in the "#region-main" "css_element"
+ And I log out
+ And I log in as "manager1"
+ And I am on site homepage
+ And I follow "Course 1"
+ When I navigate to "Participants" node in "Current course > C1"
+ And I follow "Student 2"
+ Then I should see "Preferences" in the "#region-main" "css_element"
+
+ @javascript
+ Scenario: A user with the appropriate permissions can view another user's permissions page.
+ Given I log in as "admin"
+ And I am on site homepage
+ And I follow "Turn editing on"
+ And I add the "Mentees" block
+ And I navigate to "Define roles" node in "Site administration > Users > Permissions"
+ And I click on "Add a new role" "button"
+ And I click on "Continue" "button"
+ And I set the following fields to these values:
+ | Short name | Parent |
+ | Custom full name | Parent |
+ | contextlevel30 | 1 |
+ | moodle/user:editprofile | 1 |
+ | moodle/user:viewalldetails | 1 |
+ | moodle/user:viewuseractivitiesreport | 1 |
+ | moodle/user:viewdetails | 1 |
+ And I click on "Create this role" "button"
+ And I navigate to "Browse list of users" node in "Site administration > Users > Accounts"
+ And I follow "Student 1"
+ And I click on "Preferences" "link" in the ".profile_tree" "css_element"
+ And I follow "Assign roles relative to this user"
+ And I follow "Parent"
+ And I click on "//select[@id='addselect']/descendant::option[contains(., 'Parent 1 (parent1@example.com)')]" "xpath_element"
+ And I click on "Add" "button"
+ And I log out
+ And I log in as "parent"
+ And I am on site homepage
+ When I follow "Student 1"
+ Then I should see "Preferences" in the "#region-main" "css_element"
defined('MOODLE_INTERNAL') || die();
-$version = 2015091800.01; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2015092201.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.