Merge branch 'MDL-50132-master' of git://github.com/jleyva/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 22 Sep 2015 11:46:39 +0000 (12:46 +0100)
committerDan Poltawski <dan@moodle.com>
Tue, 22 Sep 2015 11:46:39 +0000 (12:46 +0100)
82 files changed:
.jshintrc
admin/cli/install.php
admin/tool/task/cli/schedule_task.php
admin/tool/templatelibrary/amd/build/display.min.js
admin/tool/templatelibrary/amd/build/search.min.js
admin/tool/templatelibrary/amd/src/display.js
admin/tool/templatelibrary/amd/src/search.js
admin/user.php
course/delete.php
course/editsection.php
course/format/topics/tests/behat/edit_delete_sections.feature
course/format/weeks/tests/behat/edit_delete_sections.feature
course/tests/behat/create_delete_course.feature
enrol/flatfile/lib.php
enrol/self/classes/empty_form.php [new file with mode: 0644]
enrol/self/lib.php
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js
filter/glossary/yui/src/autolinker/js/autolinker.js
lang/en/my.php
lib/amd/build/event.min.js [new file with mode: 0644]
lib/amd/build/first.min.js
lib/amd/build/templates.min.js
lib/amd/src/event.js [new file with mode: 0644]
lib/amd/src/first.js
lib/amd/src/templates.js
lib/blocklib.php
lib/classes/message/inbound/handler.php
lib/cronlib.php
lib/db/install.xml
lib/db/upgrade.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js
lib/editor/atto/plugins/equation/yui/src/button/js/button.js
lib/javascript-static.js
lib/myprofilelib.php
lib/navigationlib.php
lib/outputrequirementslib.php
lib/tests/blocklib_test.php
lib/tests/fixtures/messageinbound/gmail.test [new file with mode: 0644]
lib/tests/fixtures/messageinbound/outlook.test
lib/tests/messageinbound_test.php
lib/tests/navigationlib_test.php
lib/upgrade.txt
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js
lib/yui/src/dragdrop/js/dragdrop.js
mod/data/backup/moodle2/backup_data_stepslib.php
mod/data/classes/external.php
mod/data/db/install.xml
mod/data/db/upgrade.php
mod/data/edit.php
mod/data/field/latlong/field.class.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/mod_form.php
mod/data/tests/behat/manageapproved.feature [new file with mode: 0644]
mod/data/tests/behat/required_entries.feature
mod/data/tests/externallib_test.php
mod/data/tests/lib_test.php
mod/data/version.php
mod/data/view.php
mod/forum/externallib.php
mod/forum/tests/externallib_test.php
mod/forum/upgrade.txt
mod/workshop/form/edit_form.php
mod/workshop/form/rubric/edit_form.php
mod/workshop/form/rubric/lang/en/workshopform_rubric.php
my/indexsys.php
my/lib.php
my/tests/behat/reset_all_pages.feature [new file with mode: 0644]
question/classes/bank/view.php
tag/tests/behat/edit_tag.feature
user/preferences.php
user/profilesys.php
user/tests/behat/view_preferences_page.feature [new file with mode: 0644]
version.php

index 8b8a806..ee94a05 100644 (file)
--- a/.jshintrc
+++ b/.jshintrc
@@ -35,7 +35,8 @@
     "plusplus":     false,
     "predef": [
         "M",
-        "define"
+        "define",
+        "require"
     ],
     "proto":        false,
     "regexdash":    false,
index 131056d..51364b9 100644 (file)
@@ -273,7 +273,8 @@ $interactive = empty($options['non-interactive']);
 
 // 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;
 }
 
@@ -295,23 +296,34 @@ echo get_string('cliinstallheader', 'install', $CFG->target_release)."\n";
 //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;
index 659ee2e..545e190 100644 (file)
@@ -110,7 +110,8 @@ if ($execute = $options['execute']) {
     $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');
@@ -138,7 +139,7 @@ if ($execute = $options['execute']) {
             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);
@@ -148,7 +149,7 @@ if ($execute = $options['execute']) {
         }
         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:");
index 49c0afe..55ae25d 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/display.min.js and b/admin/tool/templatelibrary/amd/build/display.min.js differ
index 869d27b..b8458ae 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/search.min.js and b/admin/tool/templatelibrary/amd/build/search.min.js differ
index bb2f77a..05507ee 100644 (file)
@@ -99,9 +99,7 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
         }
         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) {
index efefb26..2c9d828 100644 (file)
@@ -32,8 +32,8 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
      */
     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);
     };
 
index b05838c..2dffa22 100644 (file)
             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) {
index 31d4817..1ddc12d 100644 (file)
@@ -76,11 +76,12 @@ $strdeletecoursecheck = get_string("deletecoursecheck");
 $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;
index 76b49eb..ea4724a 100644 (file)
@@ -66,7 +66,7 @@ if ($deletesection) {
             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);
index 6b6cf0b..4b036d8 100644 (file)
@@ -43,7 +43,7 @@ Feature: Sections can be edited and deleted in topics format
   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
@@ -51,7 +51,7 @@ Feature: Sections can be edited and deleted in topics format
 
   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"
@@ -63,7 +63,7 @@ Feature: Sections can be edited and deleted in topics format
     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
@@ -77,7 +77,7 @@ Feature: Sections can be edited and deleted in topics format
     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
index 76d4004..c8b1022 100644 (file)
@@ -45,7 +45,7 @@ Feature: Sections can be edited and deleted 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-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
@@ -54,7 +54,7 @@ Feature: Sections can be edited and deleted in weeks format
   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"
@@ -66,7 +66,7 @@ Feature: Sections can be edited and deleted in weeks format
     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
@@ -80,7 +80,7 @@ Feature: Sections can be edited and deleted in weeks format
     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
index 1c74098..fba44dd 100644 (file)
@@ -54,7 +54,7 @@ Feature: Test we can both create and delete a course.
     # 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"
@@ -93,7 +93,7 @@ Feature: Test we can both create and delete a course.
     # 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"
index 24b3cee..57438fb 100644 (file)
@@ -441,7 +441,7 @@ class enrol_flatfile_plugin extends enrol_plugin {
             $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);
                 }
diff --git a/enrol/self/classes/empty_form.php b/enrol/self/classes/empty_form.php
new file mode 100644 (file)
index 0000000..a54d70d
--- /dev/null
@@ -0,0 +1,41 @@
+<?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);
+    }
+}
index 5c668ef..6dca20b 100644 (file)
@@ -234,8 +234,8 @@ class enrol_self_plugin extends enrol_plugin {
 
         $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) {
@@ -243,14 +243,19 @@ class enrol_self_plugin extends enrol_plugin {
                     $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);
     }
 
     /**
index b932ef7..ecffc3c 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js differ
index 34d316d..46b748d 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js differ
index b932ef7..ecffc3c 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js differ
index 564f348..3689b4a 100644 (file)
@@ -20,46 +20,53 @@ Y.extend(AUTOLINKER, Y.Base, {
     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,
@@ -76,7 +83,8 @@ Y.extend(AUTOLINKER, Y.Base, {
                     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.
index b1c8f86..ea1e734 100644 (file)
@@ -30,7 +30,11 @@ $string['pinblocksexplan'] = 'Any block settings you configure here will be visi
 $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';
diff --git a/lib/amd/build/event.min.js b/lib/amd/build/event.min.js
new file mode 100644 (file)
index 0000000..86a6407
Binary files /dev/null and b/lib/amd/build/event.min.js differ
index 9dc3a61..7d7c42d 100644 (file)
Binary files a/lib/amd/build/first.min.js and b/lib/amd/build/first.min.js differ
index c75432b..71f74a7 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
diff --git a/lib/amd/src/event.js b/lib/amd/src/event.js
new file mode 100644 (file)
index 0000000..ce20865
--- /dev/null
@@ -0,0 +1,52 @@
+// 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 });
+            });
+        },
+
+    };
+});
index 704c0c5..72646f0 100644 (file)
  * 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');
+    });
+});
index a152e03..6e4bc0c 100644 (file)
@@ -30,9 +30,10 @@ define([ 'core/mustache',
          '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.
 
@@ -326,6 +327,50 @@ define([ 'core/mustache',
         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.
         /**
@@ -379,12 +424,28 @@ define([ 'core/mustache',
          * 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);
         }
     };
 });
index 4866dc2..56208b6 100644 (file)
@@ -2055,6 +2055,31 @@ function blocks_delete_instance($instance, $nolongerused = false, $skipblockstab
     }
 }
 
+/**
+ * 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.
  *
index fcf9c37..9da1f0b 100644 (file)
@@ -245,7 +245,6 @@ abstract class handler {
      * @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 {
@@ -258,8 +257,6 @@ abstract class handler {
             return array($text, $messageformat);
         }
 
-        // Remove extra line. "Xyz wrote on...".
-        $count = 0;
         $i = 0;
         $flag = false;
         foreach ($splitted as $i => $element) {
@@ -271,9 +268,6 @@ abstract class handler {
                     $element = $splitted[$j];
                     if (!empty($element)) {
                         unset($splitted[$j]);
-                        $count++;
-                    }
-                    if ($count == $linecount) {
                         break;
                     }
                 }
@@ -282,10 +276,8 @@ abstract class handler {
         }
         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);
@@ -311,24 +303,4 @@ abstract class handler {
         }
         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;
-    }
 }
index e6e6b46..fa8bb71 100644 (file)
@@ -64,7 +64,8 @@ function cron_run() {
     // 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();
@@ -79,7 +80,7 @@ function cron_run() {
                 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()) {
@@ -90,7 +91,7 @@ function cron_run() {
                 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:");
index 837a755..9a0f927 100644 (file)
@@ -1,5 +1,5 @@
 <?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>
index 221943a..fd98221 100644 (file)
@@ -4558,5 +4558,19 @@ function xmldb_main_upgrade($oldversion) {
         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;
 }
index 9d7b767..70e8a6e 100644 (file)
@@ -1502,10 +1502,13 @@ class mysqli_native_moodle_database extends moodle_database {
     /**
      * 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
@@ -1517,14 +1520,24 @@ class mysqli_native_moodle_database extends moodle_database {
         $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'";
         }
     }
 
index b064d99..8b16565 100644 (file)
@@ -3870,6 +3870,11 @@ class core_dml_testcase extends database_driver_testcase {
         $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);
index 628f2ce..270e0eb 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js differ
index 05522fd..5740db9 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js differ
index 0350ece..ade5d5f 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js differ
index 864f421..2b30130 100644 (file)
@@ -30,7 +30,6 @@
  * @class Button
  * @extends M.editor_atto.EditorPlugin
  */
-
 var COMPONENTNAME = 'atto_equation',
     LOGNAME = 'atto_equation',
     CSS = {
@@ -229,8 +228,10 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
 
         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);
@@ -494,7 +495,10 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         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());
+            });
         }
     },
 
index 8b6af7c..2641a9f 100644 (file)
@@ -343,8 +343,7 @@ M.util.init_maximised_embed = function(Y, id) {
     };
 
     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) {
@@ -360,13 +359,16 @@ M.util.init_maximised_embed = function(Y, id) {
             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();
+        });
+    });
 };
 
 /**
index a341fbf..4c5e09d 100644 (file)
@@ -35,7 +35,7 @@ defined('MOODLE_INTERNAL') || die();
  * @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();
@@ -101,8 +101,8 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
         }
     }
 
-    // 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);
index 19284f7..ec79f51 100644 (file)
@@ -4773,6 +4773,33 @@ class settings_navigation extends navigation_node {
     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;
+    }
 }
 
 /**
index e0f0962..85fb414 100644 (file)
@@ -1636,8 +1636,8 @@ class page_requirements_manager {
         $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);
 
index 38e3790..533eeaa 100644 (file)
@@ -478,6 +478,70 @@ class core_blocklib_testcase extends advanced_testcase {
         $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.
+    }
+
 }
 
 /**
diff --git a/lib/tests/fixtures/messageinbound/gmail.test b/lib/tests/fixtures/messageinbound/gmail.test
new file mode 100644 (file)
index 0000000..a3356a8
--- /dev/null
@@ -0,0 +1,148 @@
+----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 &lt;<a href=
+=3D"mailto:nxtmorg@gmail.com">nxtmorg@gmail.com</a>&gt; 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&amp;=
+course=3D3" target=3D"_blank"><img src=3D"http://dan.moodle.local/sites/the=
+me/image.php?theme=3Dmoodleorgcleaned_moodleorg&amp;component=3Dcore&amp;im=
+age=3Du%2Ff2&amp;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&amp;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&amp;=
+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&amp;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--
index ea4cd1d..5a48804 100644 (file)
@@ -18,6 +18,7 @@ Sending mail via clent and it seems to go all good...
 Havent tried this before and it is awesome.... 
 
 Cheers
+ Rajesh
 
 ----FULLSOURCE----
 Delivered-To: moodlehqtest+aaaaaaaaaaiaaaaaaaaabqaaaaaaaaazd63zvl6kcy04ioh+@example.com
index 8baf461..0d4b50f 100644 (file)
@@ -163,10 +163,6 @@ class test_handler extends \core\message\inbound\handler {
         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() {}
index eab6b13..de22b4e 100644 (file)
@@ -450,6 +450,45 @@ class core_navigationlib_testcase extends advanced_testcase {
         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
index 3ff4060..82e1b15 100644 (file)
@@ -10,6 +10,7 @@ information provided here is intended especially for developers.
   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
index 47fc0f6..d979ca0 100644 (file)
Binary files a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js and b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js differ
index 27dec90..1e72909 100644 (file)
Binary files a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js and b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js differ
index 47fc0f6..d979ca0 100644 (file)
Binary files a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js and b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js differ
index e7700d0..55d6ca4 100644 (file)
@@ -337,7 +337,7 @@ Y.extend(DRAGDROP, Y.Base, {
      */
     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 () {
index 385e66b..a190eb5 100644 (file)
@@ -43,7 +43,7 @@ class backup_data_activity_structure_step extends backup_activity_structure_step
             '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'));
 
index d45977d..eb0b2ad 100644 (file)
@@ -152,7 +152,7 @@ class mod_data_external extends external_api {
 
                     $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.
@@ -212,6 +212,7 @@ class mod_data_external extends external_api {
                             '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),
index c4a5c2c..3eea246 100644 (file)
@@ -31,6 +31,7 @@
         <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"/>
index 602552a..fd34e52 100644 (file)
@@ -153,5 +153,20 @@ function xmldb_data_upgrade($oldversion) {
     // 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;
 }
index 7350cca..df09006 100644 (file)
@@ -112,7 +112,7 @@ $groupmode = groups_get_activity_groupmode($cm);
 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)) {
index 0bb0221..a72ebe1 100644 (file)
@@ -253,4 +253,26 @@ class data_field_latlong extends data_field_base {
         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');
+    }
 }
index cf3f7dc..76406e7 100644 (file)
@@ -208,6 +208,7 @@ $string['invalidurl'] = 'The URL you just entered is not valid';
 $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';
@@ -215,6 +216,8 @@ $string['latlongotherfields'] = 'Other fields';
 $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.';
index d664107..0fe4a23 100644 (file)
@@ -1230,9 +1230,6 @@ function data_print_template($template, $records, $data, $search='', $page=0, $r
     }
     $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
@@ -1250,7 +1247,7 @@ function data_print_template($template, $records, $data, $search='', $page=0, $r
     // 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.'&amp;rid='.$record->id.'&amp;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='
@@ -2173,6 +2170,44 @@ function data_user_can_add_entry($data, $currentgroup, $groupmode, $context = nu
     }
 }
 
+/**
+ * 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
  *
@@ -3314,6 +3349,7 @@ function data_presets_generate_xml($course, $cm, $data) {
         'maxentries',
         'rssarticles',
         'approval',
+        'manageapproved',
         'defaultsortdir'
     );
 
@@ -3838,6 +3874,7 @@ function data_process_submission(stdClass $mod, $fields, stdClass $datarecord) {
     // Empty form checking - you can't submit an empty form.
     $emptyform = true;
     $requiredfieldsfilled = true;
+    $fieldsvalidated = true;
 
     // Store the notifications.
     $result->generalnotifications = array();
@@ -3875,6 +3912,14 @@ function data_process_submission(stdClass $mod, $fields, stdClass $datarecord) {
 
         $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.
@@ -3906,7 +3951,7 @@ function data_process_submission(stdClass $mod, $fields, stdClass $datarecord) {
         $result->generalnotifications[] = get_string('emptyaddform', 'data');
     }
 
-    $result->validated = $requiredfieldsfilled && !$emptyform;
+    $result->validated = $requiredfieldsfilled && !$emptyform && $fieldsvalidated;
 
     return $result;
 }
index 6a25ea3..fb6fc19 100644 (file)
@@ -32,6 +32,11 @@ class mod_data_mod_form extends moodleform_mod {
         $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'))+
diff --git a/mod/data/tests/behat/manageapproved.feature b/mod/data/tests/behat/manageapproved.feature
new file mode 100644 (file)
index 0000000..70e4151
--- /dev/null
@@ -0,0 +1,91 @@
+@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
index 0ddad87..d5ef8e5 100644 (file)
@@ -204,3 +204,24 @@ Feature: Users can be required to specify certain fields when adding entries to
        | 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"
index 1cc34c2..a0ecd29 100644 (file)
@@ -144,7 +144,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $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') {
index fbd744c..7ea4ae5 100644 (file)
@@ -232,6 +232,267 @@ class mod_data_lib_testcase extends advanced_testcase {
         $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().
      *
index 27eeca5..08ed9c7 100644 (file)
@@ -24,7 +24,7 @@
 
 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;
index 6ff22b7..b0c5077 100644 (file)
 
 /// 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');
index 325ba2a..0f9c826 100644 (file)
@@ -61,60 +61,41 @@ class mod_forum_external extends external_api {
         $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;
             }
         }
 
@@ -155,7 +136,8 @@ class mod_forum_external extends external_api {
                     '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'
             )
         );
index 97972d6..563429a 100644 (file)
@@ -88,6 +88,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         $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;
@@ -97,6 +98,8 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         $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',
@@ -155,6 +158,13 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         $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));
index 9df95f6..03a0e13 100644 (file)
@@ -1,6 +1,10 @@
 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
index 89c31fb..708cdc4 100644 (file)
@@ -80,6 +80,23 @@ class workshop_edit_strategy_form extends moodleform {
         $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.
      *
@@ -89,4 +106,14 @@ class workshop_edit_strategy_form extends moodleform {
         // 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();
+    }
 }
index 79acc42..ccd4c29 100644 (file)
@@ -103,4 +103,48 @@ class workshop_edit_rubric_strategy_form extends workshop_edit_strategy_form {
         $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;
+    }
 }
index 9c2b5e3..3785c4f 100644 (file)
@@ -33,5 +33,6 @@ $string['layoutgrid'] = 'Grid';
 $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';
index f21e5ac..c3c2c12 100644 (file)
@@ -39,7 +39,7 @@ require_once(dirname(__FILE__) . '/../config.php');
 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();
 
@@ -48,6 +48,11 @@ $header = "$SITE->shortname: ".get_string('myhome')." (".get_string('mypage', 'a
 $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');
 
@@ -61,6 +66,11 @@ if (!$currentpage = my_get_page(null, MY_PAGE_PRIVATE)) {
 }
 $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');
index 017abc2..bd3219d 100644 (file)
@@ -136,6 +136,57 @@ function my_reset_page($userid, $private=MY_PAGE_PRIVATE, $pagetype='my-index')
     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
diff --git a/my/tests/behat/reset_all_pages.feature b/my/tests/behat/reset_all_pages.feature
new file mode 100644 (file)
index 0000000..6070126
--- /dev/null
@@ -0,0 +1,129 @@
+@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"
index 04e076d..42c17cf 100644 (file)
@@ -923,7 +923,8 @@ class view {
             $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;
         }
index 3058ecf..1a717ba 100644 (file)
@@ -140,8 +140,6 @@ Feature: Users can edit tags to add description or rename
     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
index f0895ca..d4b736e 100644 (file)
@@ -33,11 +33,6 @@ if (isguestuser()) {
 $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)) {
@@ -53,10 +48,16 @@ $PAGE->set_heading(fullname($user));
 
 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);
index 63dd426..302f5cd 100644 (file)
@@ -31,7 +31,7 @@ require_once(dirname(__FILE__) . '/../config.php');
 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();
 
@@ -40,6 +40,11 @@ $header = "$SITE->shortname: ".get_string('publicprofile')." (".get_string('mypr
 $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');
 
@@ -53,6 +58,9 @@ if (!$currentpage = my_get_page(null, MY_PAGE_PUBLIC)) {
 }
 $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();
 
diff --git a/user/tests/behat/view_preferences_page.feature b/user/tests/behat/view_preferences_page.feature
new file mode 100644 (file)
index 0000000..4d40f89
--- /dev/null
@@ -0,0 +1,85 @@
+@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"
index 260b8fc..0e4f4f0 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2015091800.01;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2015092201.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.