"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>
'mod_scorm_insert_scorm_tracks',
'mod_scorm_get_scorm_sco_tracks',
'mod_scorm_get_scorm_attempt_count',
+ 'mod_scorm_get_scorms_by_courses',
'mod_page_view_page',
'mod_resource_view_resource',
'mod_folder_view_folder',
'mod_chat_view_chat',
'mod_chat_get_chats_by_courses',
'mod_book_view_book',
+ 'mod_book_get_books_by_courses',
'mod_choice_get_choice_results',
'mod_choice_get_choice_options',
'mod_choice_submit_choice_response',
'mod_choice_view_choice',
+ 'mod_choice_get_choices_by_courses',
'mod_imscp_view_imscp',
+ 'mod_imscp_get_imscps_by_courses',
),
'enabled' => 0,
'restrictedusers' => 0,
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 () {
);
}
+ /**
+ * Describes the parameters for get_books_by_courses.
+ *
+ * @return external_external_function_parameters
+ * @since Moodle 3.0
+ */
+ public static function get_books_by_courses_parameters() {
+ return new external_function_parameters (
+ array(
+ 'courseids' => new external_multiple_structure(
+ new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, array()
+ ),
+ )
+ );
+ }
+
+ /**
+ * Returns a list of books in a provided list of courses,
+ * if no list is provided all books that the user can view will be returned.
+ *
+ * @param array $courseids the course ids
+ * @return array of books details
+ * @since Moodle 3.0
+ */
+ public static function get_books_by_courses($courseids = array()) {
+ global $CFG;
+
+ $returnedbooks = array();
+ $warnings = array();
+
+ $params = self::validate_parameters(self::get_books_by_courses_parameters(), array('courseids' => $courseids));
+
+ if (empty($params['courseids'])) {
+ $params['courseids'] = array_keys(enrol_get_my_courses());
+ }
+
+ // Ensure there are courseids to loop through.
+ if (!empty($params['courseids'])) {
+
+ list($courses, $warnings) = external_util::validate_courses($params['courseids']);
+
+ // Get the books in this course, this function checks users visibility permissions.
+ // We can avoid then additional validate_context calls.
+ $books = get_all_instances_in_courses("book", $courses);
+ foreach ($books as $book) {
+ $context = context_module::instance($book->coursemodule);
+ // Entry to return.
+ $bookdetails = array();
+ // First, we return information that any user can see in the web interface.
+ $bookdetails['id'] = $book->id;
+ $bookdetails['coursemodule'] = $book->coursemodule;
+ $bookdetails['course'] = $book->course;
+ $bookdetails['name'] = format_string($book->name, true, array('context' => $context));
+ // Format intro.
+ list($bookdetails['intro'], $bookdetails['introformat']) =
+ external_format_text($book->intro, $book->introformat, $context->id, 'mod_book', 'intro', null);
+ $bookdetails['numbering'] = $book->numbering;
+ $bookdetails['navstyle'] = $book->navstyle;
+ $bookdetails['customtitles'] = $book->customtitles;
+
+ if (has_capability('moodle/course:manageactivities', $context)) {
+ $bookdetails['revision'] = $book->revision;
+ $bookdetails['timecreated'] = $book->timecreated;
+ $bookdetails['timemodified'] = $book->timemodified;
+ $bookdetails['section'] = $book->section;
+ $bookdetails['visible'] = $book->visible;
+ $bookdetails['groupmode'] = $book->groupmode;
+ $bookdetails['groupingid'] = $book->groupingid;
+ }
+ $returnedbooks[] = $bookdetails;
+ }
+ }
+ $result = array();
+ $result['books'] = $returnedbooks;
+ $result['warnings'] = $warnings;
+ return $result;
+ }
+
+ /**
+ * Describes the get_books_by_courses return value.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.0
+ */
+ public static function get_books_by_courses_returns() {
+ return new external_single_structure(
+ array(
+ 'books' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'id' => new external_value(PARAM_INT, 'Book id'),
+ 'coursemodule' => new external_value(PARAM_INT, 'Course module id'),
+ 'course' => new external_value(PARAM_INT, 'Course id'),
+ 'name' => new external_value(PARAM_TEXT, 'Book name'),
+ 'intro' => new external_value(PARAM_RAW, 'The Book intro'),
+ 'introformat' => new external_format_value('intro'),
+ 'numbering' => new external_value(PARAM_INT, 'Book numbering configuration'),
+ 'navstyle' => new external_value(PARAM_INT, 'Book navigation style configuration'),
+ 'customtitles' => new external_value(PARAM_INT, 'Book custom titles type'),
+ 'revision' => new external_value(PARAM_INT, 'Book revision', VALUE_OPTIONAL),
+ 'timecreated' => new external_value(PARAM_INT, 'Time of creation', VALUE_OPTIONAL),
+ 'timemodified' => new external_value(PARAM_INT, 'Time of last modification', VALUE_OPTIONAL),
+ 'section' => new external_value(PARAM_INT, 'Course section id', VALUE_OPTIONAL),
+ 'visible' => new external_value(PARAM_BOOL, 'Visible', VALUE_OPTIONAL),
+ 'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
+ 'groupingid' => new external_value(PARAM_INT, 'Group id', VALUE_OPTIONAL),
+ ), 'Books'
+ )
+ ),
+ 'warnings' => new external_warnings(),
+ )
+ );
+ }
+
}
'capabilities' => 'mod/book:read'
),
+ 'mod_book_get_books_by_courses' => array(
+ 'classname' => 'mod_book_external',
+ 'methodname' => 'get_books_by_courses',
+ 'description' => 'Returns a list of book instances in a provided set of courses,
+ if no courses are provided then all the book instances the user has access to will be returned.',
+ 'type' => 'read',
+ 'capabilities' => ''
+ )
);
}
}
+
+ /**
+ * Test get_books_by_courses
+ */
+ public function test_get_books_by_courses() {
+ global $DB, $USER;
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $course1 = self::getDataGenerator()->create_course();
+ $bookoptions1 = array(
+ 'course' => $course1->id,
+ 'name' => 'First Book'
+ );
+ $book1 = self::getDataGenerator()->create_module('book', $bookoptions1);
+ $course2 = self::getDataGenerator()->create_course();
+ $bookoptions2 = array(
+ 'course' => $course2->id,
+ 'name' => 'Second Book'
+ );
+ $book2 = self::getDataGenerator()->create_module('book', $bookoptions2);
+ $student1 = $this->getDataGenerator()->create_user();
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+
+ // Enroll Student1 in Course1.
+ self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id);
+ $this->setUser($student1);
+
+ $books = mod_book_external::get_books_by_courses();
+ // We need to execute the return values cleaning process to simulate the web service server.
+ $books = external_api::clean_returnvalue(mod_book_external::get_books_by_courses_returns(), $books);
+ $this->assertCount(1, $books['books']);
+ $this->assertEquals('First Book', $books['books'][0]['name']);
+ // We see 9 fields.
+ $this->assertCount(9, $books['books'][0]);
+
+ // As Student you cannot see some book properties like 'section'.
+ $this->assertFalse(isset($books['books'][0]['section']));
+
+ // Student1 is not enrolled in course2. The webservice will return a warning!
+ $books = mod_book_external::get_books_by_courses(array($course2->id));
+ // We need to execute the return values cleaning process to simulate the web service server.
+ $books = external_api::clean_returnvalue(mod_book_external::get_books_by_courses_returns(), $books);
+ $this->assertCount(0, $books['books']);
+ $this->assertEquals(1, $books['warnings'][0]['warningcode']);
+
+ // Now as admin.
+ $this->setAdminUser();
+ // As Admin we can see this book.
+ $books = mod_book_external::get_books_by_courses(array($course2->id));
+ // We need to execute the return values cleaning process to simulate the web service server.
+ $books = external_api::clean_returnvalue(mod_book_external::get_books_by_courses_returns(), $books);
+
+ $this->assertCount(1, $books['books']);
+ $this->assertEquals('Second Book', $books['books'][0]['name']);
+ // We see 16 fields.
+ $this->assertCount(16, $books['books'][0]);
+ // As an Admin you can see some book properties like 'section'.
+ $this->assertEquals(0, $books['books'][0]['section']);
+
+ // Enrol student in the second course.
+ self::getDataGenerator()->enrol_user($student1->id, $course2->id, $studentrole->id);
+ $this->setUser($student1);
+ $books = mod_book_external::get_books_by_courses();
+ $books = external_api::clean_returnvalue(mod_book_external::get_books_by_courses_returns(), $books);
+ $this->assertCount(2, $books['books']);
+
+ }
}
defined('MOODLE_INTERNAL') || die;
$plugin->component = 'mod_book'; // Full name of the plugin (used for diagnostics)
-$plugin->version = 2015051101; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2015051102; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2015050500; // Requires this Moodle version
$plugin->cron = 0; // Period for cron to check this module (secs)
);
}
+ /**
+ * Describes the parameters for get_choices_by_courses.
+ *
+ * @return external_external_function_parameters
+ * @since Moodle 3.0
+ */
+ public static function get_choices_by_courses_parameters() {
+ return new external_function_parameters (
+ array(
+ 'courseids' => new external_multiple_structure(
+ new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, array()
+ ),
+ )
+ );
+ }
+
+ /**
+ * Returns a list of choices in a provided list of courses,
+ * if no list is provided all choices that the user can view will be returned.
+ *
+ * @param array $courseids the course ids
+ * @return array of choices details
+ * @since Moodle 3.0
+ */
+ public static function get_choices_by_courses($courseids = array()) {
+ global $CFG;
+
+ $returnedchoices = array();
+ $warnings = array();
+
+ $params = self::validate_parameters(self::get_choices_by_courses_parameters(), array('courseids' => $courseids));
+
+ if (empty($params['courseids'])) {
+ $params['courseids'] = array_keys(enrol_get_my_courses());
+ }
+
+ // Ensure there are courseids to loop through.
+ if (!empty($params['courseids'])) {
+
+ list($courses, $warnings) = external_util::validate_courses($params['courseids']);
+
+ // Get the choices in this course, this function checks users visibility permissions.
+ // We can avoid then additional validate_context calls.
+ $choices = get_all_instances_in_courses("choice", $courses);
+ foreach ($choices as $choice) {
+ $context = context_module::instance($choice->coursemodule);
+ // Entry to return.
+ $choicedetails = array();
+ // First, we return information that any user can see in the web interface.
+ $choicedetails['id'] = $choice->id;
+ $choicedetails['coursemodule'] = $choice->coursemodule;
+ $choicedetails['course'] = $choice->course;
+ $choicedetails['name'] = format_string($choice->name, true, array('context' => $context));;
+ // Format intro.
+ list($choicedetails['intro'], $choicedetails['introformat']) =
+ external_format_text($choice->intro, $choice->introformat,
+ $context->id, 'mod_choice', 'intro', null);
+
+ if (has_capability('mod/choice:choose', $context)) {
+ $choicedetails['publish'] = $choice->publish;
+ $choicedetails['showresults'] = $choice->showresults;
+ $choicedetails['showpreview'] = $choice->showpreview;
+ $choicedetails['timeopen'] = $choice->timeopen;
+ $choicedetails['timeclose'] = $choice->timeclose;
+ $choicedetails['display'] = $choice->display;
+ $choicedetails['allowupdate'] = $choice->allowupdate;
+ $choicedetails['allowmultiple'] = $choice->allowmultiple;
+ $choicedetails['limitanswers'] = $choice->limitanswers;
+ $choicedetails['showunanswered'] = $choice->showunanswered;
+ $choicedetails['includeinactive'] = $choice->includeinactive;
+ }
+
+ if (has_capability('moodle/course:manageactivities', $context)) {
+ $choicedetails['timemodified'] = $choice->timemodified;
+ $choicedetails['completionsubmit'] = $choice->completionsubmit;
+ $choicedetails['section'] = $choice->section;
+ $choicedetails['visible'] = $choice->visible;
+ $choicedetails['groupmode'] = $choice->groupmode;
+ $choicedetails['groupingid'] = $choice->groupingid;
+ }
+ $returnedchoices[] = $choicedetails;
+ }
+ }
+ $result = array();
+ $result['choices'] = $returnedchoices;
+ $result['warnings'] = $warnings;
+ return $result;
+ }
+
+ /**
+ * Describes the mod_choice_get_choices_by_courses return value.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.0
+ */
+ public static function get_choices_by_courses_returns() {
+ return new external_single_structure(
+ array(
+ 'choices' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'id' => new external_value(PARAM_INT, 'Choice instance id'),
+ 'coursemodule' => new external_value(PARAM_INT, 'Course module id'),
+ 'course' => new external_value(PARAM_INT, 'Course id'),
+ 'name' => new external_value(PARAM_TEXT, 'Choice name'),
+ 'intro' => new external_value(PARAM_RAW, 'The choice intro'),
+ 'introformat' => new external_format_value('intro'),
+ 'publish' => new external_value(PARAM_BOOL, 'If choice is published', VALUE_OPTIONAL),
+ 'showresults' => new external_value(PARAM_INT, '0 never, 1 after answer, 2 after close, 3 always',
+ VALUE_OPTIONAL),
+ 'display' => new external_value(PARAM_INT, 'Display mode (vertical, horizontal)', VALUE_OPTIONAL),
+ 'allowupdate' => new external_value(PARAM_BOOL, 'Allow update', VALUE_OPTIONAL),
+ 'allowmultiple' => new external_value(PARAM_BOOL, 'Allow multiple choices', VALUE_OPTIONAL),
+ 'showunanswered' => new external_value(PARAM_BOOL, 'Show users who not answered yet', VALUE_OPTIONAL),
+ 'includeinactive' => new external_value(PARAM_BOOL, 'Include inactive users', VALUE_OPTIONAL),
+ 'limitanswers' => new external_value(PARAM_BOOL, 'Limit unswers', VALUE_OPTIONAL),
+ 'timeopen' => new external_value(PARAM_INT, 'Date of opening validity', VALUE_OPTIONAL),
+ 'timeclose' => new external_value(PARAM_INT, 'Date of closing validity', VALUE_OPTIONAL),
+ 'showpreview' => new external_value(PARAM_BOOL, 'Show preview before timeopen', VALUE_OPTIONAL),
+ 'timemodified' => new external_value(PARAM_INT, 'Time of last modification', VALUE_OPTIONAL),
+ 'completionsubmit' => new external_value(PARAM_BOOL, 'Completion on user submission', VALUE_OPTIONAL),
+ 'section' => new external_value(PARAM_INT, 'Course section id', VALUE_OPTIONAL),
+ 'visible' => new external_value(PARAM_BOOL, 'Visible', VALUE_OPTIONAL),
+ 'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
+ 'groupingid' => new external_value(PARAM_INT, 'Group id', VALUE_OPTIONAL),
+ ), 'Choices'
+ )
+ ),
+ 'warnings' => new external_warnings(),
+ )
+ );
+ }
+
}
'type' => 'write',
'capabilities' => ''
),
+
+ 'mod_choice_get_choices_by_courses' => array(
+ 'classname' => 'mod_choice_external',
+ 'methodname' => 'get_choices_by_courses',
+ 'description' => 'Returns a list of choice instances in a provided set of courses,
+ if no courses are provided then all the choice instances the user has access to will be returned.',
+ 'type' => 'read',
+ 'capabilities' => ''
+ ),
);
$this->assertNotEmpty($event->get_name());
}
+
+ /**
+ * Test get_choices_by_courses
+ */
+ public function test_get_choices_by_courses() {
+ global $DB;
+ $this->resetAfterTest(true);
+ // As admin.
+ $this->setAdminUser();
+ $course1 = self::getDataGenerator()->create_course();
+ $choiceoptions1 = array(
+ 'course' => $course1->id,
+ 'name' => 'First IMSCP'
+ );
+ $choice1 = self::getDataGenerator()->create_module('choice', $choiceoptions1);
+ $course2 = self::getDataGenerator()->create_course();
+
+ $choiceoptions2 = array(
+ 'course' => $course2->id,
+ 'name' => 'Second IMSCP'
+ );
+ $choice2 = self::getDataGenerator()->create_module('choice', $choiceoptions2);
+ $student1 = $this->getDataGenerator()->create_user();
+
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+
+ // Enroll Student1 in Course1.
+ self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id);
+
+ $this->setUser($student1);
+ $choices = mod_choice_external::get_choices_by_courses(array());
+ $choices = external_api::clean_returnvalue(mod_choice_external::get_choices_by_courses_returns(), $choices);
+ $this->assertCount(1, $choices['choices']);
+ $this->assertEquals('First IMSCP', $choices['choices'][0]['name']);
+ // As Student you cannot see some IMSCP properties like 'section'.
+ $this->assertFalse(isset($choices['choices'][0]['section']));
+
+ // Student1 is not enrolled in this Course.
+ // The webservice will give a warning!
+ $choices = mod_choice_external::get_choices_by_courses(array($course2->id));
+ $choices = external_api::clean_returnvalue(mod_choice_external::get_choices_by_courses_returns(), $choices);
+ $this->assertCount(0, $choices['choices']);
+ $this->assertEquals(1, $choices['warnings'][0]['warningcode']);
+
+ // Now as admin.
+ $this->setAdminUser();
+ // As Admin we can see this IMSCP.
+ $choices = mod_choice_external::get_choices_by_courses(array($course2->id));
+ $choices = external_api::clean_returnvalue(mod_choice_external::get_choices_by_courses_returns(), $choices);
+ $this->assertCount(1, $choices['choices']);
+ $this->assertEquals('Second IMSCP', $choices['choices'][0]['name']);
+ // As an Admin you can see some IMSCP properties like 'section'.
+ $this->assertEquals(0, $choices['choices'][0]['section']);
+
+ // Now, prohibit capabilities.
+ $this->setUser($student1);
+ $contextcourse1 = context_course::instance($course1->id);
+ // Prohibit capability = mod:choice:choose on Course1 for students.
+ assign_capability('mod/choice:choose', CAP_PROHIBIT, $studentrole->id, $contextcourse1->id);
+ accesslib_clear_all_caches_for_unit_testing();
+
+ $choices = mod_choice_external::get_choices_by_courses(array($course1->id));
+ $choices = external_api::clean_returnvalue(mod_choice_external::get_choices_by_courses_returns(), $choices);
+ $this->assertFalse(isset($choices['choices'][0]['timeopen']));
+ }
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015051101; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2015051102; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2015050500; // Requires this Moodle version
$plugin->component = 'mod_choice'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
'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
);
}
+ /**
+ * Describes the parameters for get_imscps_by_courses.
+ *
+ * @return external_external_function_parameters
+ * @since Moodle 3.0
+ */
+ public static function get_imscps_by_courses_parameters() {
+ return new external_function_parameters (
+ array(
+ 'courseids' => new external_multiple_structure(
+ new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, array()
+ ),
+ )
+ );
+ }
+
+ /**
+ * Returns a list of IMSCP packages in a provided list of courses,
+ * if no list is provided all IMSCP packages that the user can view will be returned.
+ *
+ * @param array $courseids the course ids
+ * @return array of IMSCP packages details and possible warnings
+ * @since Moodle 3.0
+ */
+ public static function get_imscps_by_courses($courseids = array()) {
+ global $CFG;
+
+ $returnedimscps = array();
+ $warnings = array();
+
+ $params = self::validate_parameters(self::get_imscps_by_courses_parameters(), array('courseids' => $courseids));
+
+ if (empty($params['courseids'])) {
+ $params['courseids'] = array_keys(enrol_get_my_courses());
+ }
+
+ // Ensure there are courseids to loop through.
+ if (!empty($params['courseids'])) {
+
+ list($courses, $warnings) = external_util::validate_courses($params['courseids']);
+
+ // Get the imscps in this course, this function checks users visibility permissions.
+ // We can avoid then additional validate_context calls.
+ $imscps = get_all_instances_in_courses("imscp", $courses);
+ foreach ($imscps as $imscp) {
+ $context = context_module::instance($imscp->coursemodule);
+
+ // Entry to return.
+ $imscpdetails = array();
+ // First, we return information that any user can see in the web interface.
+ $imscpdetails['id'] = $imscp->id;
+ $imscpdetails['coursemodule'] = $imscp->coursemodule;
+ $imscpdetails['course'] = $imscp->course;
+ $imscpdetails['name'] = format_string($imscp->name, true, array('context' => $context));
+
+ if (has_capability('mod/imscp:view', $context)) {
+ // Format intro.
+ list($imscpdetails['intro'], $imscpdetails['introformat']) =
+ external_format_text($imscp->intro, $imscp->introformat, $context->id, 'mod_imscp', 'intro', null);
+ }
+
+ if (has_capability('moodle/course:manageactivities', $context)) {
+ $imscpdetails['revision'] = $imscp->revision;
+ $imscpdetails['keepold'] = $imscp->keepold;
+ $imscpdetails['structure'] = $imscp->structure;
+ $imscpdetails['timemodified'] = $imscp->timemodified;
+ $imscpdetails['section'] = $imscp->section;
+ $imscpdetails['visible'] = $imscp->visible;
+ $imscpdetails['groupmode'] = $imscp->groupmode;
+ $imscpdetails['groupingid'] = $imscp->groupingid;
+ }
+ $returnedimscps[] = $imscpdetails;
+ }
+ }
+ $result = array();
+ $result['imscps'] = $returnedimscps;
+ $result['warnings'] = $warnings;
+ return $result;
+ }
+
+ /**
+ * Describes the get_imscps_by_courses return value.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.0
+ */
+ public static function get_imscps_by_courses_returns() {
+ return new external_single_structure(
+ array(
+ 'imscps' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'id' => new external_value(PARAM_INT, 'IMSCP id'),
+ 'coursemodule' => new external_value(PARAM_INT, 'Course module id'),
+ 'course' => new external_value(PARAM_INT, 'Course id'),
+ 'name' => new external_value(PARAM_TEXT, 'Activity name'),
+ 'intro' => new external_value(PARAM_RAW, 'The IMSCP intro', VALUE_OPTIONAL),
+ 'introformat' => new external_format_value('intro', VALUE_OPTIONAL),
+ 'revision' => new external_value(PARAM_INT, 'Revision', VALUE_OPTIONAL),
+ 'keepold' => new external_value(PARAM_INT, 'Number of old IMSCP to keep', VALUE_OPTIONAL),
+ 'structure' => new external_value(PARAM_RAW, 'IMSCP structure', VALUE_OPTIONAL),
+ 'timemodified' => new external_value(PARAM_RAW, 'Time of last modification', VALUE_OPTIONAL),
+ 'section' => new external_value(PARAM_INT, 'Course section id', VALUE_OPTIONAL),
+ 'visible' => new external_value(PARAM_BOOL, 'If visible', VALUE_OPTIONAL),
+ 'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
+ 'groupingid' => new external_value(PARAM_INT, 'Group id', VALUE_OPTIONAL),
+ ), 'IMS content packages'
+ )
+ ),
+ 'warnings' => new external_warnings(),
+ )
+ );
+ }
+
}
'capabilities' => 'mod/imscp:view'
),
+ 'mod_imscp_get_imscps_by_courses' => array(
+ 'classname' => 'mod_imscp_external',
+ 'methodname' => 'get_imscps_by_courses',
+ 'description' => 'Returns a list of IMSCP instances in a provided set of courses,
+ if no courses are provided then all the IMSCP instances the user has access to will be returned.',
+ 'type' => 'read',
+ 'capabilities' => 'moodle/imscp:view'
+ ),
+
);
}
}
+
+ /**
+ * Test get_imscps_by_courses
+ */
+ public function test_get_imscps_by_courses() {
+ global $DB, $USER;
+ $this->resetAfterTest(true);
+ // As admin.
+ $this->setAdminUser();
+ $course1 = self::getDataGenerator()->create_course();
+ $imscpoptions1 = array(
+ 'course' => $course1->id,
+ 'name' => 'First IMSCP'
+ );
+ $imscp1 = self::getDataGenerator()->create_module('imscp', $imscpoptions1);
+ $course2 = self::getDataGenerator()->create_course();
+
+ $imscpoptions2 = array(
+ 'course' => $course2->id,
+ 'name' => 'Second IMSCP'
+ );
+ $imscp2 = self::getDataGenerator()->create_module('imscp', $imscpoptions2);
+ $student1 = $this->getDataGenerator()->create_user();
+
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+
+ // Enroll Student1 in Course1.
+ self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id);
+
+ $this->setUser($student1);
+ $imscps = mod_imscp_external::get_imscps_by_courses(array());
+ $imscps = external_api::clean_returnvalue(mod_imscp_external::get_imscps_by_courses_returns(), $imscps);
+ $this->assertCount(1, $imscps['imscps']);
+ $this->assertEquals('First IMSCP', $imscps['imscps'][0]['name']);
+ // As Student you cannot see some IMSCP properties like 'section'.
+ $this->assertFalse(isset($imscps['imscps'][0]['section']));
+
+ // Student1 is not enrolled in this Course.
+ // The webservice will give a warning!
+ $imscps = mod_imscp_external::get_imscps_by_courses(array($course2->id));
+ $imscps = external_api::clean_returnvalue(mod_imscp_external::get_imscps_by_courses_returns(), $imscps);
+ $this->assertCount(0, $imscps['imscps']);
+ $this->assertEquals(1, $imscps['warnings'][0]['warningcode']);
+
+ // Now as admin.
+ $this->setAdminUser();
+ // As Admin we can see this IMSCP.
+ $imscps = mod_imscp_external::get_imscps_by_courses(array($course2->id));
+ $imscps = external_api::clean_returnvalue(mod_imscp_external::get_imscps_by_courses_returns(), $imscps);
+ $this->assertCount(1, $imscps['imscps']);
+ $this->assertEquals('Second IMSCP', $imscps['imscps'][0]['name']);
+ // As an Admin you can see some IMSCP properties like 'section'.
+ $this->assertEquals(0, $imscps['imscps'][0]['section']);
+
+ // Now, prohibit capabilities.
+ $this->setUser($student1);
+ $contextcourse1 = context_course::instance($course1->id);
+ // Prohibit capability = mod:imscp:view on Course1 for students.
+ assign_capability('mod/imscp:view', CAP_PROHIBIT, $studentrole->id, $contextcourse1->id);
+ accesslib_clear_all_caches_for_unit_testing();
+
+ $imscps = mod_imscp_external::get_imscps_by_courses(array($course1->id));
+ $imscps = external_api::clean_returnvalue(mod_imscp_external::get_imscps_by_courses_returns(), $imscps);
+ $this->assertFalse(isset($imscps['imscps'][0]['intro']));
+ }
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015051101; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2015051102; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2015050500; // Requires this Moodle version.
$plugin->component = 'mod_imscp'; // Full name of the plugin (used for diagnostics).
$plugin->cron = 0;
)
);
}
+
+ /**
+ * Describes the parameters for get_scorms_by_courses.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.0
+ */
+ public static function get_scorms_by_courses_parameters() {
+ return new external_function_parameters (
+ array(
+ 'courseids' => new external_multiple_structure(
+ new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, array()
+ ),
+ )
+ );
+ }
+
+ /**
+ * Returns a list of scorms in a provided list of courses,
+ * if no list is provided all scorms that the user can view will be returned.
+ *
+ * @param array $courseids the course ids
+ * @return array the scorm details
+ * @since Moodle 3.0
+ */
+ public static function get_scorms_by_courses($courseids = array()) {
+ global $CFG;
+
+ $returnedscorms = array();
+ $warnings = array();
+
+ $params = self::validate_parameters(self::get_scorms_by_courses_parameters(), array('courseids' => $courseids));
+
+ if (empty($params['courseids'])) {
+ $params['courseids'] = array_keys(enrol_get_my_courses());
+ }
+
+ // Ensure there are courseids to loop through.
+ if (!empty($params['courseids'])) {
+
+ list($courses, $warnings) = external_util::validate_courses($params['courseids']);
+
+ // Get the scorms in this course, this function checks users visibility permissions.
+ // We can avoid then additional validate_context calls.
+ $scorms = get_all_instances_in_courses("scorm", $courses);
+
+ $fs = get_file_storage();
+ foreach ($scorms as $scorm) {
+
+ $context = context_module::instance($scorm->coursemodule);
+
+ // Entry to return.
+ $module = array();
+
+ // First, we return information that any user can see in (or can deduce from) the web interface.
+ $module['id'] = $scorm->id;
+ $module['coursemodule'] = $scorm->coursemodule;
+ $module['course'] = $scorm->course;
+ $module['name'] = format_string($scorm->name, true, array('context' => $context));
+ list($module['intro'], $module['introformat']) =
+ external_format_text($scorm->intro, $scorm->introformat, $context->id, 'mod_scorm', 'intro', $scorm->id);
+
+ // Check if the SCORM open and return warnings if so.
+ list($open, $openwarnings) = scorm_get_availability_status($scorm, true, $context);
+
+ if (!$open) {
+ foreach ($openwarnings as $warningkey => $warningdata) {
+ $warnings[] = array(
+ 'item' => 'scorm',
+ 'itemid' => $scorm->id,
+ 'warningcode' => $warningkey,
+ 'message' => get_string($warningkey, 'scorm', $warningdata)
+ );
+ }
+ } else {
+ $module['packagesize'] = 0;
+ // SCORM size.
+ if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
+ if ($packagefile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, '/', $scorm->reference)) {
+ $module['packagesize'] = $packagefile->get_filesize();
+ // Download URL.
+ $module['packageurl'] = moodle_url::make_webservice_pluginfile_url(
+ $context->id, 'mod_scorm', 'package', 0, '/', $scorm->reference)->out(false);
+ }
+ }
+
+ $viewablefields = array('version', 'maxgrade', 'grademethod', 'whatgrade', 'maxattempt', 'forcecompleted',
+ 'forcenewattempt', 'lastattemptlock', 'displayattemptstatus', 'displaycoursestructure',
+ 'sha1hash', 'md5hash', 'revision', 'launch', 'skipview', 'hidebrowse', 'hidetoc', 'nav',
+ 'navpositionleft', 'navpositiontop', 'auto', 'popup', 'width', 'height', 'timeopen',
+ 'timeclose', 'displayactivityname', 'scormtype', 'reference');
+
+ // Check additional permissions for returning optional private settings.
+ if (has_capability('moodle/course:manageactivities', $context)) {
+
+ $additionalfields = array('updatefreq', 'options', 'completionstatusrequired', 'completionscorerequired',
+ 'autocommit', 'timemodified', 'section', 'visible', 'groupmode', 'groupingid');
+ $viewablefields = array_merge($viewablefields, $additionalfields);
+
+ }
+
+ foreach ($viewablefields as $field) {
+ $module[$field] = $scorm->{$field};
+ }
+ }
+
+ $returnedscorms[] = $module;
+ }
+ }
+
+ $result = array();
+ $result['scorms'] = $returnedscorms;
+ $result['warnings'] = $warnings;
+ return $result;
+ }
+
+ /**
+ * Describes the get_scorms_by_courses return value.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.0
+ */
+ public static function get_scorms_by_courses_returns() {
+
+ return new external_single_structure(
+ array(
+ 'scorms' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'id' => new external_value(PARAM_INT, 'SCORM id'),
+ 'coursemodule' => new external_value(PARAM_INT, 'Course module id'),
+ 'course' => new external_value(PARAM_INT, 'Course id'),
+ 'name' => new external_value(PARAM_TEXT, 'SCORM name'),
+ 'intro' => new external_value(PARAM_RAW, 'The SCORM intro'),
+ 'introformat' => new external_format_value('intro'),
+ 'packagesize' => new external_value(PARAM_INT, 'SCORM zip package size', VALUE_OPTIONAL),
+ 'packageurl' => new external_value(PARAM_URL, 'SCORM zip package URL', VALUE_OPTIONAL),
+ 'version' => new external_value(PARAM_NOTAGS, 'SCORM version (SCORM_12, SCORM_13, SCORM_AICC)',
+ VALUE_OPTIONAL),
+ 'maxgrade' => new external_value(PARAM_INT, 'Max grade', VALUE_OPTIONAL),
+ 'grademethod' => new external_value(PARAM_INT, 'Grade method', VALUE_OPTIONAL),
+ 'whatgrade' => new external_value(PARAM_INT, 'What grade', VALUE_OPTIONAL),
+ 'maxattempt' => new external_value(PARAM_INT, 'Maximum number of attemtps', VALUE_OPTIONAL),
+ 'forcecompleted' => new external_value(PARAM_BOOL, 'Status current attempt is forced to "completed"',
+ VALUE_OPTIONAL),
+ 'forcenewattempt' => new external_value(PARAM_BOOL, 'Hides the "Start new attempt" checkbox',
+ VALUE_OPTIONAL),
+ 'lastattemptlock' => new external_value(PARAM_BOOL, 'Prevents to launch new attempts once finished',
+ VALUE_OPTIONAL),
+ 'displayattemptstatus' => new external_value(PARAM_BOOL, 'Display attempts status', VALUE_OPTIONAL),
+ 'displaycoursestructure' => new external_value(PARAM_BOOL, 'Display contents structure',
+ VALUE_OPTIONAL),
+ 'sha1hash' => new external_value(PARAM_NOTAGS, 'Package content or ext path hash', VALUE_OPTIONAL),
+ 'md5hash' => new external_value(PARAM_NOTAGS, 'MD5 Hash of package file', VALUE_OPTIONAL),
+ 'revision' => new external_value(PARAM_INT, 'Revison number', VALUE_OPTIONAL),
+ 'launch' => new external_value(PARAM_INT, 'First content to launch', VALUE_OPTIONAL),
+ 'skipview' => new external_value(PARAM_BOOL, 'Skip or not content structure page', VALUE_OPTIONAL),
+ 'hidebrowse' => new external_value(PARAM_BOOL, 'Disable preview mode?', VALUE_OPTIONAL),
+ 'hidetoc' => new external_value(PARAM_BOOL, 'Display or not course structure in player',
+ VALUE_OPTIONAL),
+ 'nav' => new external_value(PARAM_INT, 'Show navigation buttons', VALUE_OPTIONAL),
+ 'navpositionleft' => new external_value(PARAM_INT, 'Navigation position left', VALUE_OPTIONAL),
+ 'navpositiontop' => new external_value(PARAM_INT, 'Navigation position top', VALUE_OPTIONAL),
+ 'auto' => new external_value(PARAM_BOOL, 'Auto continue?', VALUE_OPTIONAL),
+ 'popup' => new external_value(PARAM_INT, 'Display in current or new window', VALUE_OPTIONAL),
+ 'width' => new external_value(PARAM_INT, 'Frame width', VALUE_OPTIONAL),
+ 'height' => new external_value(PARAM_INT, 'Frame height', VALUE_OPTIONAL),
+ 'timeopen' => new external_value(PARAM_INT, 'Available from', VALUE_OPTIONAL),
+ 'timeclose' => new external_value(PARAM_INT, 'Available to', VALUE_OPTIONAL),
+ 'displayactivityname' => new external_value(PARAM_BOOL, 'Display the activity name above the player?',
+ VALUE_OPTIONAL),
+ 'scormtype' => new external_value(PARAM_ALPHA, 'SCORM type', VALUE_OPTIONAL),
+ 'reference' => new external_value(PARAM_NOTAGS, 'Reference to the package', VALUE_OPTIONAL),
+ 'updatefreq' => new external_value(PARAM_INT, 'Auto-update frequency for remote packages',
+ VALUE_OPTIONAL),
+ 'options' => new external_value(PARAM_RAW, 'Additional options', VALUE_OPTIONAL),
+ 'completionstatusrequired' => new external_value(PARAM_INT, 'Status passed/completed required?',
+ VALUE_OPTIONAL),
+ 'completionscorerequired' => new external_value(PARAM_INT, 'Minimum score required', VALUE_OPTIONAL),
+ 'autocommit' => new external_value(PARAM_BOOL, 'Save track data automatically?', VALUE_OPTIONAL),
+ 'timemodified' => new external_value(PARAM_INT, 'Time of last modification', VALUE_OPTIONAL),
+ 'section' => new external_value(PARAM_INT, 'Course section id', VALUE_OPTIONAL),
+ 'visible' => new external_value(PARAM_BOOL, 'Visible', VALUE_OPTIONAL),
+ 'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
+ 'groupingid' => new external_value(PARAM_INT, 'Group id', VALUE_OPTIONAL),
+ ), 'SCORM'
+ )
+ ),
+ 'warnings' => new external_warnings(),
+ )
+ );
+ }
}
'type' => 'read',
'capabilities' => ''
),
+
+ 'mod_scorm_get_scorms_by_courses' => array(
+ 'classname' => 'mod_scorm_external',
+ 'methodname' => 'get_scorms_by_courses',
+ 'description' => 'Returns a list of scorm instances in a provided set of courses, if
+ no courses are provided then all the scorm instances the user has access to will be returned.',
+ 'type' => 'read',
+ 'capabilities' => ''
+ )
);
$this->assertEquals('invaliduser', $e->errorcode);
}
}
+
+ /*
+ * Test get scorms by courses
+ */
+ public function test_mod_scorm_get_scorms_by_courses() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+
+ // Create users.
+ $student = self::getDataGenerator()->create_user();
+ $teacher = self::getDataGenerator()->create_user();
+
+ // Set to the student user.
+ self::setUser($student);
+
+ // Create courses to add the modules.
+ $course1 = self::getDataGenerator()->create_course();
+ $course2 = self::getDataGenerator()->create_course();
+
+ // First scorm.
+ $record = new stdClass();
+ $record->introformat = FORMAT_HTML;
+ $record->course = $course1->id;
+ $scorm1 = self::getDataGenerator()->create_module('scorm', $record);
+
+ // Second scorm.
+ $record = new stdClass();
+ $record->introformat = FORMAT_HTML;
+ $record->course = $course2->id;
+ $scorm2 = self::getDataGenerator()->create_module('scorm', $record);
+
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+ $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+
+ // Users enrolments.
+ $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id, 'manual');
+ $this->getDataGenerator()->enrol_user($teacher->id, $course1->id, $teacherrole->id, 'manual');
+
+ // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
+ $enrol = enrol_get_plugin('manual');
+ $enrolinstances = enrol_get_instances($course2->id, true);
+ foreach ($enrolinstances as $courseenrolinstance) {
+ if ($courseenrolinstance->enrol == "manual") {
+ $instance2 = $courseenrolinstance;
+ break;
+ }
+ }
+ $enrol->enrol_user($instance2, $student->id, $studentrole->id);
+
+ $returndescription = mod_scorm_external::get_scorms_by_courses_returns();
+
+ // Test open/close dates.
+
+ $timenow = time();
+ $scorm1->timeopen = $timenow - DAYSECS;
+ $scorm1->timeclose = $timenow - HOURSECS;
+ $DB->update_record('scorm', $scorm1);
+
+ $result = mod_scorm_external::get_scorms_by_courses(array($course1->id));
+ $result = external_api::clean_returnvalue($returndescription, $result);
+ $this->assertCount(1, $result['warnings']);
+ // Only 'id', 'coursemodule', 'course', 'name', 'intro', 'introformat'.
+ $this->assertCount(6, $result['scorms'][0]);
+ $this->assertEquals('expired', $result['warnings'][0]['warningcode']);
+
+ $scorm1->timeopen = $timenow + DAYSECS;
+ $scorm1->timeclose = $scorm1->timeopen + DAYSECS;
+ $DB->update_record('scorm', $scorm1);
+
+ $result = mod_scorm_external::get_scorms_by_courses(array($course1->id));
+ $result = external_api::clean_returnvalue($returndescription, $result);
+ $this->assertCount(1, $result['warnings']);
+ // Only 'id', 'coursemodule', 'course', 'name', 'intro', 'introformat'.
+ $this->assertCount(6, $result['scorms'][0]);
+ $this->assertEquals('notopenyet', $result['warnings'][0]['warningcode']);
+
+ // Reset times.
+ $scorm1->timeopen = 0;
+ $scorm1->timeclose = 0;
+ $DB->update_record('scorm', $scorm1);
+
+ // Create what we expect to be returned when querying the two courses.
+ // First for the student user.
+ $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'version', 'maxgrade',
+ 'grademethod', 'whatgrade', 'maxattempt', 'forcecompleted', 'forcenewattempt', 'lastattemptlock',
+ 'displayattemptstatus', 'displaycoursestructure', 'sha1hash', 'md5hash', 'revision', 'launch',
+ 'skipview', 'hidebrowse', 'hidetoc', 'nav', 'navpositionleft', 'navpositiontop', 'auto',
+ 'popup', 'width', 'height', 'timeopen', 'timeclose', 'displayactivityname', 'packagesize',
+ 'packageurl', 'scormtype', 'reference');
+
+ // Add expected coursemodule and data.
+ $scorm1->coursemodule = $scorm1->cmid;
+ $scorm1->section = 0;
+ $scorm1->visible = true;
+ $scorm1->groupmode = 0;
+ $scorm1->groupingid = 0;
+
+ $scorm2->coursemodule = $scorm2->cmid;
+ $scorm2->section = 0;
+ $scorm2->visible = true;
+ $scorm2->groupmode = 0;
+ $scorm2->groupingid = 0;
+
+ // SCORM size. The same package is used in both SCORMs.
+ $scormcontext1 = context_module::instance($scorm1->cmid);
+ $scormcontext2 = context_module::instance($scorm2->cmid);
+ $fs = get_file_storage();
+ $packagefile = $fs->get_file($scormcontext1->id, 'mod_scorm', 'package', 0, '/', $scorm1->reference);
+ $packagesize = $packagefile->get_filesize();
+
+ $packageurl1 = moodle_url::make_webservice_pluginfile_url(
+ $scormcontext1->id, 'mod_scorm', 'package', 0, '/', $scorm1->reference)->out(false);
+ $packageurl2 = moodle_url::make_webservice_pluginfile_url(
+ $scormcontext2->id, 'mod_scorm', 'package', 0, '/', $scorm2->reference)->out(false);
+
+ $scorm1->packagesize = $packagesize;
+ $scorm1->packageurl = $packageurl1;
+ $scorm2->packagesize = $packagesize;
+ $scorm2->packageurl = $packageurl2;
+
+ $expected1 = array();
+ $expected2 = array();
+ foreach ($expectedfields as $field) {
+
+ // Since we return the fields used as boolean as PARAM_BOOL instead PARAM_INT we need to force casting here.
+ // From the returned fields definition we obtain the type expected for the field.
+ $fieldtype = $returndescription->keys['scorms']->content->keys[$field]->type;
+ if ($fieldtype == PARAM_BOOL) {
+ $expected1[$field] = (bool) $scorm1->{$field};
+ $expected2[$field] = (bool) $scorm2->{$field};
+ } else {
+ $expected1[$field] = $scorm1->{$field};
+ $expected2[$field] = $scorm2->{$field};
+ }
+ }
+
+ $expectedscorms = array();
+ $expectedscorms[] = $expected2;
+ $expectedscorms[] = $expected1;
+
+ // Call the external function passing course ids.
+ $result = mod_scorm_external::get_scorms_by_courses(array($course2->id, $course1->id));
+ $result = external_api::clean_returnvalue($returndescription, $result);
+ $this->assertEquals($expectedscorms, $result['scorms']);
+
+ // Call the external function without passing course id.
+ $result = mod_scorm_external::get_scorms_by_courses();
+ $result = external_api::clean_returnvalue($returndescription, $result);
+ $this->assertEquals($expectedscorms, $result['scorms']);
+
+ // Unenrol user from second course and alter expected scorms.
+ $enrol->unenrol_user($instance2, $student->id);
+ array_shift($expectedscorms);
+
+ // Call the external function without passing course id.
+ $result = mod_scorm_external::get_scorms_by_courses();
+ $result = external_api::clean_returnvalue($returndescription, $result);
+ $this->assertEquals($expectedscorms, $result['scorms']);
+
+ // Call for the second course we unenrolled the user from, expected warning.
+ $result = mod_scorm_external::get_scorms_by_courses(array($course2->id));
+ $this->assertCount(1, $result['warnings']);
+ $this->assertEquals('1', $result['warnings'][0]['warningcode']);
+ $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
+
+ // Now, try as a teacher for getting all the additional fields.
+ self::setUser($teacher);
+
+ $additionalfields = array('updatefreq', 'timemodified', 'options',
+ 'completionstatusrequired', 'completionscorerequired', 'autocommit',
+ 'section', 'visible', 'groupmode', 'groupingid');
+
+ foreach ($additionalfields as $field) {
+ $fieldtype = $returndescription->keys['scorms']->content->keys[$field]->type;
+
+ if ($fieldtype == PARAM_BOOL) {
+ $expectedscorms[0][$field] = (bool) $scorm1->{$field};
+ } else {
+ $expectedscorms[0][$field] = $scorm1->{$field};
+ }
+ }
+
+ $result = mod_scorm_external::get_scorms_by_courses();
+ $result = external_api::clean_returnvalue($returndescription, $result);
+ $this->assertEquals($expectedscorms, $result['scorms']);
+
+ // Even with the SCORM closed in time teacher should retrieve the info.
+ $scorm1->timeopen = $timenow - DAYSECS;
+ $scorm1->timeclose = $timenow - HOURSECS;
+ $DB->update_record('scorm', $scorm1);
+
+ $expectedscorms[0]['timeopen'] = $scorm1->timeopen;
+ $expectedscorms[0]['timeclose'] = $scorm1->timeclose;
+
+ $result = mod_scorm_external::get_scorms_by_courses();
+ $result = external_api::clean_returnvalue($returndescription, $result);
+ $this->assertEquals($expectedscorms, $result['scorms']);
+
+ // Admin also should get all the information.
+ self::setAdminUser();
+
+ $result = mod_scorm_external::get_scorms_by_courses(array($course1->id));
+ $result = external_api::clean_returnvalue($returndescription, $result);
+ $this->assertEquals($expectedscorms, $result['scorms']);
+ }
}
$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.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2015092204.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.