Merge branch 'MDL-50529_dupe' of git://github.com/andyjdavis/moodle
authorDavid Monllao <davidm@moodle.com>
Wed, 2 Sep 2015 01:58:17 +0000 (09:58 +0800)
committerDavid Monllao <davidm@moodle.com>
Wed, 2 Sep 2015 01:58:17 +0000 (09:58 +0800)
163 files changed:
admin/index.php
admin/roles/ajax.php [new file with mode: 0644]
admin/roles/classes/capability_table_base.php
admin/roles/classes/capability_table_with_risks.php
admin/roles/classes/check_capability_table.php
admin/roles/classes/permissions_table.php
admin/roles/permissions.php
admin/tool/customlang/edit.php
admin/tool/customlang/renderer.php
admin/tool/health/lang/en/tool_health.php
admin/tool/installaddon/classes/validator.php
admin/tool/installaddon/lang/en/tool_installaddon.php
admin/tool/installaddon/tests/fixtures/nocomponent/baz/lang/en/auth_baz.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/nocomponent/baz/version.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/nolang/bah/version.php
admin/tool/installaddon/tests/fixtures/plugindir/foobar/version.php
admin/tool/installaddon/tests/fixtures/plugindir/legacymod/lang/en/legacymod.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/plugindir/legacymod/version.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/wronglang/bah/lang/en/bah.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/wronglang/bah/version.php [new file with mode: 0644]
admin/tool/installaddon/tests/validator_test.php
admin/tool/installaddon/version.php
admin/tool/langimport/lang/en/tool_langimport.php
admin/tool/langimport/tests/behat/manage_langpacks.feature
admin/tool/task/cli/schedule_task.php
auth/cas/auth.php
blocks/messages/block_messages.php
blocks/tag_youtube/lang/en/block_tag_youtube.php
blocks/tests/behat/configure_block_throughout_site.feature
config-dist.php
course/renderer.php
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js
course/yui/src/toolboxes/js/resource.js
enrol/meta/classes/observer.php
grade/lib.php
grade/report/grader/lib.php
grade/report/singleview/lib.php
grade/report/user/lib.php
grade/tests/behat/grade_point_maximum.feature
lang/en/admin.php
lang/en/grades.php
lang/en/hub.php
lang/en/media.php
lang/en/message.php
lang/en/moodle.php
lang/en/question.php
lang/en/role.php
lib/adminlib.php
lib/ajax/blocks.php
lib/amd/build/permissionmanager.min.js [new file with mode: 0644]
lib/amd/build/templates.min.js
lib/amd/src/permissionmanager.js [new file with mode: 0644]
lib/amd/src/templates.js
lib/blocklib.php
lib/classes/plugin_manager.php
lib/classes/session/manager.php
lib/classes/useragent.php
lib/cronlib.php
lib/db/install.xml
lib/db/upgrade.php
lib/deprecatedlib.php
lib/editor/atto/autosave-ajax.php
lib/editor/atto/classes/task/autosave_cleanup_task.php [new file with mode: 0644]
lib/editor/atto/db/tasks.php [new file with mode: 0644]
lib/editor/atto/lang/en/editor_atto.php
lib/editor/atto/lib.php
lib/editor/atto/tests/behat/autosave.feature [new file with mode: 0644]
lib/editor/atto/version.php
lib/editorlib.php
lib/form/editor.php
lib/formslib.php
lib/myprofilelib.php
lib/navigationlib.php
lib/outputlib.php
lib/outputrenderers.php
lib/phpunit/classes/util.php
lib/rsslib.php
lib/setuplib.php
lib/templates/permissionmanager_panelcontent.mustache [new file with mode: 0644]
lib/templates/permissionmanager_role.mustache [new file with mode: 0644]
lib/tests/setuplib_test.php
lib/tests/useragent_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-debug.js
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-min.js
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu.js
lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js
lib/yui/build/moodle-core-dock/moodle-core-dock-min.js
lib/yui/build/moodle-core-dock/moodle-core-dock.js
lib/yui/src/actionmenu/js/actionmenu.js
lib/yui/src/dock/js/block.js
lib/yui/src/dock/js/dock.js
login/signup.php
login/signup_form.php
mod/book/lang/en/book.php
mod/chat/lang/en/chat.php
mod/choice/lang/en/choice.php
mod/data/field/textarea/field.class.php
mod/data/templates.php
mod/forum/classes/post_form.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/tests/behat/groups_in_course_no_groups_in_forum.feature [new file with mode: 0644]
mod/forum/tests/behat/no_groups_in_course.feature [new file with mode: 0644]
mod/forum/tests/behat/separate_group_discussions.feature
mod/forum/tests/behat/separate_group_single_group_discussions.feature
mod/forum/tests/behat/visible_group_discussions.feature [new file with mode: 0644]
mod/lesson/lang/en/lesson.php
mod/lesson/mod_form.php
mod/quiz/attemptlib.php
mod/quiz/review.php
mod/quiz/reviewquestion.php
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/tests/behat/manually_mark_question.feature [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js
mod/quiz/yui/src/toolboxes/js/resource.js
mod/scorm/lang/en/scorm.php
mod/upgrade.txt
question/behaviour/behaviourbase.php
question/behaviour/manualgraded/tests/walkthrough_test.php
question/behaviour/rendererbase.php
question/engine/lib.php
question/engine/questionattempt.php
question/engine/tests/questionattempt_test.php
question/engine/tests/questionattempt_with_steps_test.php
question/engine/tests/questionutils_test.php
question/engine/upgrade.txt
question/type/essay/renderer.php
repository/youtube/lang/en/repository_youtube.php
tag/coursetagslib.php
tag/lib.php
tag/tests/events_test.php
tag/tests/taglib_test.php
theme/bootstrapbase/layout/columns1.php
theme/bootstrapbase/layout/columns2.php
theme/bootstrapbase/layout/columns3.php
theme/bootstrapbase/layout/popup.php
theme/bootstrapbase/layout/secure.php
theme/bootstrapbase/less/moodle/bootstrapoverride.less
theme/bootstrapbase/less/moodle/dock.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/renderers/core_renderer.php
theme/bootstrapbase/style/moodle.css
theme/clean/classes/core_renderer.php
theme/clean/layout/columns1.php
theme/clean/layout/columns2.php
theme/clean/layout/columns3.php
theme/clean/layout/secure.php
theme/clean/lib.php
theme/image.php
theme/upgrade.txt
user/lib.php
user/profile.php
user/tests/behat/view_full_profile.feature
user/tests/userlib_test.php
user/view.php
version.php

index bad3ed9..1cea8f8 100644 (file)
@@ -513,7 +513,15 @@ if (isguestuser()) {
     redirect(get_login_url());
 }
 $context = context_system::instance();
-require_capability('moodle/site:config', $context);
+
+if (!has_capability('moodle/site:config', $context)) {
+    // Do not throw exception display an empty page with administration menu if visible for current user.
+    $PAGE->set_title($SITE->fullname);
+    $PAGE->set_heading($SITE->fullname);
+    echo $OUTPUT->header();
+    echo $OUTPUT->footer();
+    exit;
+}
 
 // check that site is properly customized
 $site = get_site();
diff --git a/admin/roles/ajax.php b/admin/roles/ajax.php
new file mode 100644 (file)
index 0000000..717b8f2
--- /dev/null
@@ -0,0 +1,81 @@
+<?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/>.
+
+/**
+ * This file processes AJAX requests and returns JSON
+ *
+ * This is a server part of yui permissions manager module
+ *
+ * @package core_role
+ * @copyright 2015 Martin Mastny
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define('AJAX_SCRIPT', true);
+
+require(__DIR__ . '/../../config.php');
+
+$contextid = required_param('contextid', PARAM_INT);
+$getroles = optional_param('getroles', 0, PARAM_BOOL);
+
+list($context, $course, $cm) = get_context_info_array($contextid);
+
+require_login($course, false, $cm);
+require_capability('moodle/role:review', $context);
+require_sesskey();
+
+list($overridableroles, $overridecounts, $nameswithcounts) = get_overridable_roles($context,
+        ROLENAME_BOTH, true);
+
+if ($getroles) {
+    echo json_encode($overridableroles);
+    die();
+}
+
+$capability = required_param('capability', PARAM_CAPABILITY);
+$roleid = required_param('roleid', PARAM_INT);
+$action = required_param('action', PARAM_ALPHA);
+
+$capability = $DB->get_record('capabilities', array('name' => $capability), '*', MUST_EXIST);
+
+if (!isset($overridableroles[$roleid])) {
+    throw new moodle_exception('invalidarguments');
+}
+
+if (!has_capability('moodle/role:override', $context)) {
+    if (!has_capability('moodle/role:safeoverride', $context) || !is_safe_capability($capability)) {
+        require_capability('moodle/role:override', $context);
+    }
+}
+
+switch ($action) {
+    case 'allow':
+        role_change_permission($roleid, $context, $capability->name, CAP_ALLOW);
+        break;
+    case 'prevent':
+        role_change_permission($roleid, $context, $capability->name, CAP_PREVENT);
+        break;
+    case 'prohibit':
+        role_change_permission($roleid, $context, $capability->name, CAP_PROHIBIT);
+        break;
+    case 'unprohibit':
+        role_change_permission($roleid, $context, $capability->name, CAP_INHERIT);
+        break;
+    default:
+        throw new moodle_exception('invalidarguments');
+}
+
+echo json_encode($action);
+die();
\ No newline at end of file
index b46c137..2765659 100644 (file)
@@ -106,18 +106,22 @@ abstract class core_role_capability_table_base {
             $component = $capability->component;
 
             // Start the row.
-            echo '<tr class="' . implode(' ', array_unique(array_merge(array('rolecap'),
-                    $this->get_row_classes($capability)))) . '">';
+            $rowattributes = $this->get_row_attributes($capability);
+            // Handle class attributes same as other.
+            $rowclasses = array_unique(array_merge(array('rolecap'), $this->get_row_classes($capability)));
+            if (array_key_exists('class', $rowattributes)) {
+                $rowclasses = array_unique(array_merge($rowclasses, array($rowattributes['class'])));
+            }
+            $rowattributes['class']  = implode(' ', $rowclasses);
 
             // Table cell for the capability name.
-            echo '<th scope="row" class="name"><span class="cap-desc">' . get_capability_docs_link($capability) .
+            $contents = '<th scope="row" class="name"><span class="cap-desc">' . get_capability_docs_link($capability) .
                 '<span class="cap-name">' . $capability->name . '</span></span></th>';
 
             // Add the cells specific to this table.
-            $this->add_row_cells($capability);
+            $contents .= $this->add_row_cells($capability);
 
-            // End the row.
-            echo "</tr>\n";
+            echo html_writer::tag('tr', $contents, $rowattributes);
         }
 
         // End of the table.
@@ -167,6 +171,17 @@ abstract class core_role_capability_table_base {
         return array();
     }
 
+    /**
+     * For subclasses to override. Additional attributes to be added to
+     * each table row for the capability
+     *
+     * @param stdClass $capability the capability this row relates to.
+     * @return array attribute names and their values.
+     */
+    protected function get_row_attributes($capability) {
+        return array();
+    }
+
     /**
      * For subclasses to override. Output the data cells for this capability. The
      * capability name cell will already have been output.
@@ -174,6 +189,7 @@ abstract class core_role_capability_table_base {
      * You can rely on get_row_classes always being called before add_row_cells.
      *
      * @param stdClass $capability the capability this row relates to.
+     * @return string html of row cells
      */
     protected abstract function add_row_cells($capability);
 }
index 5fd57dd..b0480b0 100644 (file)
@@ -165,13 +165,15 @@ abstract class core_role_capability_table_with_risks extends core_role_capabilit
     protected function add_row_cells($capability) {
         $this->add_permission_cells($capability);
         // One cell for each possible risk.
+        $cells = '';
         foreach ($this->allrisks as $riskname => $risk) {
-            echo '<td class="risk ' . str_replace('risk', '', $riskname) . '">';
+            $cells .= '<td class="risk ' . str_replace('risk', '', $riskname) . '">';
             if ($risk & (int)$capability->riskbitmask) {
-                echo $this->get_risk_icon($riskname);
+                $cells .= $this->get_risk_icon($riskname);
             }
-            echo '</td>';
+            $cells .= '</td>';
         }
+        return $cells;
     }
 
     /**
index 20f9370..b716ae5 100644 (file)
@@ -79,6 +79,6 @@ class core_role_check_capability_table extends core_role_capability_table_base {
         $a->fullname = $this->fullname;
         $a->capability = $capability->name;
         $a->context = $this->contextname;
-        echo '<td>' . $result . '</td>';
+        return '<td>' . $result . '</td>';
     }
 }
index aa9a2f4..9f339d8 100644 (file)
@@ -67,6 +67,8 @@ class core_role_permissions_table extends core_role_capability_table_base {
 
     protected function add_row_cells($capability) {
         global $OUTPUT, $PAGE;
+        $renderer = $PAGE->get_renderer('core');
+        $adminurl = new moodle_url("/admin/");
 
         $context = $this->context;
         $contextid = $this->context->id;
@@ -75,7 +77,6 @@ class core_role_permissions_table extends core_role_capability_table_base {
         $overridableroles = $this->overridableroles;
         $roles = $this->roles;
 
-
         list($needed, $forbidden) = get_roles_with_cap_in_context($context, $capability->name);
         $neededroles    = array();
         $forbiddenroles = array();
@@ -91,40 +92,50 @@ class core_role_permissions_table extends core_role_capability_table_base {
 
         foreach ($roles as $id => $name) {
             if (isset($needed[$id])) {
-                $neededroles[$id] = $roles[$id];
+                $templatecontext = array("rolename" => $name, "roleid" => $id, "action" => "prevent", "spanclass" => "allowed",
+                                  "linkclass" => "preventlink", "adminurl" => $adminurl->out(), "imageurl" => "");
                 if (isset($overridableroles[$id]) and ($allowoverrides or ($allowsafeoverrides and is_safe_capability($capability)))) {
-                    $preventurl = new moodle_url($PAGE->url, array('contextid'=>$contextid, 'roleid'=>$id, 'capability'=>$capability->name, 'prevent'=>1));
-                    $neededroles[$id] .= $OUTPUT->action_icon($preventurl, new pix_icon('t/delete', get_string('prevent', 'core_role')));
+                    $templatecontext['imageurl'] = $renderer->pix_url('t/delete');
                 }
+                $neededroles[$id] = $renderer->render_from_template('core/permissionmanager_role', $templatecontext);
             }
         }
-        $neededroles = implode(', ', $neededroles);
+        $neededroles = implode(' ', $neededroles);
         foreach ($roles as $id => $name) {
             if (isset($forbidden[$id])  and ($allowoverrides or ($allowsafeoverrides and is_safe_capability($capability)))) {
-                $forbiddenroles[$id] = $roles[$id];
+                $templatecontext = array("rolename" => $name, "roleid" => $id, "action" => "unprohibit",
+                                "spanclass" => "forbidden", "linkclass" => "unprohibitlink", "adminurl" => $adminurl->out(),
+                                "imageurl" => "");
                 if (isset($overridableroles[$id]) and prohibit_is_removable($id, $context, $capability->name)) {
-                    $unprohibiturl = new moodle_url($PAGE->url, array('contextid'=>$contextid, 'roleid'=>$id, 'capability'=>$capability->name, 'unprohibit'=>1));
-                    $forbiddenroles[$id] .= $OUTPUT->action_icon($unprohibiturl, new pix_icon('t/delete', get_string('delete')));
+                    $templatecontext['imageurl'] = $renderer->pix_url('t/delete');
                 }
+                $forbiddenroles[$id] = $renderer->render_from_template('core/permissionmanager_role', $templatecontext);
             }
         }
-        $forbiddenroles = implode(', ', $forbiddenroles);
+        $forbiddenroles = implode(' ', $forbiddenroles);
 
         if ($allowable and ($allowoverrides or ($allowsafeoverrides and is_safe_capability($capability)))) {
-            $allowurl = new moodle_url($PAGE->url, array('contextid'=>$contextid, 'capability'=>$capability->name, 'allow'=>1));
-            $neededroles .= '<div class="allowmore">'.$OUTPUT->action_icon($allowurl, new pix_icon('t/add', get_string('allow', 'core_role'))).'</div>';
+            $allowurl = new moodle_url($PAGE->url, array('contextid' => $contextid,
+                                       'capability' => $capability->name, 'allow' => 1));
+            $allowicon = $OUTPUT->action_icon($allowurl, new pix_icon('t/add', get_string('allow', 'core_role')), null,
+                                            array('class' => 'allowlink', 'data-action' => 'allow'));
+            $neededroles .= html_writer::div($allowicon, 'allowmore');
         }
 
         if ($forbitable and ($allowoverrides or ($allowsafeoverrides and is_safe_capability($capability)))) {
-            $prohibiturl = new moodle_url($PAGE->url, array('contextid'=>$contextid, 'capability'=>$capability->name, 'prohibit'=>1));
-            $forbiddenroles .= '<div class="prohibitmore">'.$OUTPUT->action_icon($prohibiturl, new pix_icon('t/add', get_string('prohibit', 'core_role'))).'</div>';
+            $prohibiturl = new moodle_url($PAGE->url, array('contextid' => $contextid,
+                                          'capability' => $capability->name, 'prohibit' => 1));
+            $prohibiticon = $OUTPUT->action_icon($prohibiturl, new pix_icon('t/add', get_string('prohibit', 'core_role')), null,
+                                                array('class' => 'prohibitlink', 'data-action' => 'prohibit'));
+            $forbiddenroles .= html_writer::div($prohibiticon, 'prohibitmore');
         }
 
         $risks = $this->get_risks($capability);
 
-        echo '<td>' . $risks . '</td>';
-        echo '<td>' . $neededroles . '</td>';
-        echo '<td>' . $forbiddenroles . '</td>';
+        $contents = html_writer::tag('td', $risks, array('class' => 'risks'));
+        $contents .= html_writer::tag('td', $neededroles, array('class' => 'allowedroles'));
+        $contents .= html_writer::tag('td', $forbiddenroles, array('class' => 'forbiddenroles'));
+        return $contents;
     }
 
     protected function get_risks($capability) {
@@ -147,4 +158,18 @@ class core_role_permissions_table extends core_role_capability_table_base {
 
         return $return;
     }
+
+    /**
+     * Add additional attributes to row
+     *
+     * @param stdClass $capability capability that this table row relates to.
+     * @return array key value pairs of attribute names and values.
+     */
+    protected function get_row_attributes($capability) {
+        return array(
+                'data-id' => $capability->id,
+                'data-name' => $capability->name,
+                'data-humanname' => get_capability_string($capability->name),
+        );
+    }
 }
index 841732d..c83f819 100644 (file)
@@ -195,6 +195,15 @@ if ($capability && ($allowoverrides || ($allowsafeoverrides && is_safe_capabilit
 echo $OUTPUT->header();
 echo $OUTPUT->heading($title);
 
+$adminurl = new moodle_url('/admin/');
+$arguments = array('contextid' => $contextid,
+                'contextname' => $contextname,
+                'adminurl' => $adminurl->out());
+$PAGE->requires->strings_for_js(
+                                array('roleprohibitinfo', 'roleprohibitheader', 'roleallowinfo', 'roleallowheader',
+                                    'confirmunassigntitle', 'confirmroleunprohibit', 'confirmroleprevent', 'confirmunassignyes',
+                                    'confirmunassignno'), 'core_role');
+$PAGE->requires->js_call_amd('core/permissionmanager', 'initialize', array($arguments));
 $table = new core_role_permissions_table($context, $contextname, $allowoverrides, $allowsafeoverrides, $overridableroles);
 echo $OUTPUT->box_start('generalbox capbox');
 // Print link to advanced override page.
index 71c54fe..8ad4536 100644 (file)
@@ -63,7 +63,7 @@ if ($translatorsubmitted) {
     $checkin = optional_param('savecheckin', false, PARAM_RAW);
 
     if ($checkin === false) {
-        $nexturl = $PAGE->url;
+        $nexturl = new moodle_url($PAGE->url, array('p' => $currentpage));
     } else {
         $nexturl = new moodle_url('/admin/tool/customlang/index.php', array('action'=>'checkin', 'lng' => $lng, 'sesskey'=>sesskey()));
     }
index c733dda..224e76b 100644 (file)
@@ -133,6 +133,7 @@ class tool_customlang_renderer extends plugin_renderer_base {
         $output .= html_writer::start_tag('div');
         $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'translatorsubmitted', 'value'=>1));
         $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey()));
+        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'p', 'value'=>$translator->currentpage));
         $save1   = html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'savecontinue', 'value'=>get_string('savecontinue', 'tool_customlang')));
         $save2   = html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'savecheckin', 'value'=>get_string('savecheckin', 'tool_customlang')));
         $output .= html_writer::tag('fieldset', $save1.$save2, array('class'=>'buttonsbar'));
index 8095ab2..4120104 100644 (file)
@@ -23,7 +23,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['healthnoproblemsfound'] = 'There is no health problem found!';
+$string['healthnoproblemsfound'] = 'No health problems were found!';
 $string['healthproblemsdetected'] = 'Health problems detected!';
 $string['healthproblemsolution'] = 'Health problem solution';
 $string['healthreturntomain'] = 'Continue';
index cafd9b9..0ae7097 100644 (file)
@@ -300,29 +300,21 @@ class tool_installaddon_validator {
         $this->versionphp = array();
         $info = $this->parse_version_php($fullpath);
 
-        if ($this->assertions['plugintype'] === 'mod') {
-            $type = 'module';
-        } else {
-            $type = 'plugin';
+        if (isset($info['module->version'])) {
+            $this->add_message(self::ERROR, 'versionphpsyntax', '$module');
+            return false;
         }
 
-        if (!isset($info[$type.'->version'])) {
-            if ($type === 'module' and isset($info['plugin->version'])) {
-                // Expect the activity module using $plugin in version.php instead of $module.
-                $type = 'plugin';
-                $this->versionphp['version'] = $info[$type.'->version'];
-                $this->add_message(self::INFO, 'pluginversion', $this->versionphp['version']);
-            } else {
-                $this->add_message(self::ERROR, 'missingversion');
-                return false;
-            }
-        } else {
-            $this->versionphp['version'] = $info[$type.'->version'];
+        if (isset($info['plugin->version'])) {
+            $this->versionphp['version'] = $info['plugin->version'];
             $this->add_message(self::INFO, 'pluginversion', $this->versionphp['version']);
+        } else {
+            $this->add_message(self::ERROR, 'missingversion');
+            return false;
         }
 
-        if (isset($info[$type.'->requires'])) {
-            $this->versionphp['requires'] = $info[$type.'->requires'];
+        if (isset($info['plugin->requires'])) {
+            $this->versionphp['requires'] = $info['plugin->requires'];
             if ($this->versionphp['requires'] > $this->assertions['moodleversion']) {
                 $this->add_message(self::ERROR, 'requiresmoodle', $this->versionphp['requires']);
                 return false;
@@ -330,24 +322,27 @@ class tool_installaddon_validator {
             $this->add_message(self::INFO, 'requiresmoodle', $this->versionphp['requires']);
         }
 
-        if (isset($info[$type.'->component'])) {
-            $this->versionphp['component'] = $info[$type.'->component'];
-            list($reqtype, $reqname) = core_component::normalize_component($this->versionphp['component']);
-            if ($reqtype !== $this->assertions['plugintype']) {
-                $this->add_message(self::ERROR, 'componentmismatchtype', array(
-                    'expected' => $this->assertions['plugintype'],
-                    'found' => $reqtype));
-                return false;
-            }
-            if ($reqname !== $this->rootdir) {
-                $this->add_message(self::ERROR, 'componentmismatchname', $reqname);
-                return false;
-            }
-            $this->add_message(self::INFO, 'componentmatch', $this->versionphp['component']);
+        if (!isset($info['plugin->component'])) {
+            $this->add_message(self::ERROR, 'missingcomponent');
+            return false;
+        }
+
+        $this->versionphp['component'] = $info['plugin->component'];
+        list($reqtype, $reqname) = core_component::normalize_component($this->versionphp['component']);
+        if ($reqtype !== $this->assertions['plugintype']) {
+            $this->add_message(self::ERROR, 'componentmismatchtype', array(
+                'expected' => $this->assertions['plugintype'],
+                'found' => $reqtype));
+            return false;
+        }
+        if ($reqname !== $this->rootdir) {
+            $this->add_message(self::ERROR, 'componentmismatchname', $reqname);
+            return false;
         }
+        $this->add_message(self::INFO, 'componentmatch', $this->versionphp['component']);
 
-        if (isset($info[$type.'->maturity'])) {
-            $this->versionphp['maturity'] = $info[$type.'->maturity'];
+        if (isset($info['plugin->maturity'])) {
+            $this->versionphp['maturity'] = $info['plugin->maturity'];
             if ($this->versionphp['maturity'] === 'MATURITY_STABLE') {
                 $this->add_message(self::INFO, 'maturity', $this->versionphp['maturity']);
             } else {
@@ -355,8 +350,8 @@ class tool_installaddon_validator {
             }
         }
 
-        if (isset($info[$type.'->release'])) {
-            $this->versionphp['release'] = $info[$type.'->release'];
+        if (isset($info['plugin->release'])) {
+            $this->versionphp['release'] = $info['plugin->release'];
             $this->add_message(self::INFO, 'release', $this->versionphp['release']);
         }
 
index 6f1ad09..4f869ca 100644 (file)
@@ -71,6 +71,9 @@ $string['validationmsg_filestatus_info'] = 'Attempting to extract file {$a->file
 $string['validationmsg_foundlangfile'] = 'Found language file';
 $string['validationmsg_maturity'] = 'Declared maturity level';
 $string['validationmsg_maturity_help'] = 'The plugin can declare its maturity level. If the maintainer considers the plugin stable, the declared maturity level will read MATURITY_STABLE. All other maturity levels (such as alpha or beta) should be considered unstable and a warning is raised.';
+$string['validationmsg_missingcomponent'] = 'Plugin does not declare its component name';
+$string['validationmsg_missingcomponent_help'] = 'All plugins must provide their full component name via the `$plugin->component` declaration in the version.php file.';
+$string['validationmsg_missingcomponent_link'] = 'Development:version.php';
 $string['validationmsg_missingexpectedlangenfile'] = 'English language file name mismatch';
 $string['validationmsg_missingexpectedlangenfile_info'] = 'The given plugin type is missing the expected English language file {$a}.';
 $string['validationmsg_missinglangenfile'] = 'No English language file found';
@@ -91,6 +94,7 @@ $string['validationmsg_rootdirinvalid_help'] = 'The name of the root directory i
 $string['validationmsg_targetexists'] = 'Target location already exists';
 $string['validationmsg_targetexists_help'] = 'The directory that the plugin is to be installed to must not yet exist.';
 $string['validationmsg_unknowntype'] = 'Unknown plugin type';
+$string['validationmsg_versionphpsyntax'] = 'Unsupported syntax detected in version.php file';
 $string['validationmsglevel_debug'] = 'Debug';
 $string['validationmsglevel_error'] = 'Error';
 $string['validationmsglevel_info'] = 'OK';
diff --git a/admin/tool/installaddon/tests/fixtures/nocomponent/baz/lang/en/auth_baz.php b/admin/tool/installaddon/tests/fixtures/nocomponent/baz/lang/en/auth_baz.php
new file mode 100644 (file)
index 0000000..4422fa6
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+$string['pluginname'] = 'This is a plugin with $plugin->component missing in its version.php';
diff --git a/admin/tool/installaddon/tests/fixtures/nocomponent/baz/version.php b/admin/tool/installaddon/tests/fixtures/nocomponent/baz/version.php
new file mode 100644 (file)
index 0000000..f87367e
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+
+$plugin->version = 2015080600;
+$plugin->release = 'B.A.Z. Auth fake plugin';
+//$plugin->component is missing here so the validation must fail.
index ebad339..5b76ce2 100644 (file)
@@ -1,9 +1,7 @@
 <?php
 
-$module->version = 10; // Ignored, this should use $plugin
 $plugin->version = 2013031900;
 $plugin->component = 'local_foobar';
 $plugin->requires = 2013031200;
-$module->release = 'We are not an activity module!';
 $plugin->maturity = MATURITY_ALPHA;
 //$plugin->release = 'And this is commented';
diff --git a/admin/tool/installaddon/tests/fixtures/plugindir/legacymod/lang/en/legacymod.php b/admin/tool/installaddon/tests/fixtures/plugindir/legacymod/lang/en/legacymod.php
new file mode 100644 (file)
index 0000000..01e066e
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+$string['modulename'] = 'Legacy activity module with $module in version.php';
diff --git a/admin/tool/installaddon/tests/fixtures/plugindir/legacymod/version.php b/admin/tool/installaddon/tests/fixtures/plugindir/legacymod/version.php
new file mode 100644 (file)
index 0000000..3434317
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+
+// Support for the $module has been dropped in Moodle 3.0.
+$module->version = 2013031900;
diff --git a/admin/tool/installaddon/tests/fixtures/wronglang/bah/lang/en/bah.php b/admin/tool/installaddon/tests/fixtures/wronglang/bah/lang/en/bah.php
new file mode 100644 (file)
index 0000000..70668e8
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+$string['pluginname'] = 'This would be valid filename for module, not a block';
diff --git a/admin/tool/installaddon/tests/fixtures/wronglang/bah/version.php b/admin/tool/installaddon/tests/fixtures/wronglang/bah/version.php
new file mode 100644 (file)
index 0000000..a4fe771
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+
+$plugin->version = 2014122455;
+$plugin->component = 'block_bah';
index d0342f5..44eb306 100644 (file)
@@ -134,6 +134,28 @@ class tool_installaddon_validator_testcase extends basic_testcase {
         $this->assertFalse($validator->execute());
         $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missingversionphp'));
 
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array(
+            'legacymod/' => true,
+            'legacymod/version.php' => true,
+            'legacymod/lang/' => true,
+            'legacymod/lang/en/' => true,
+            'legacymod/lang/en/legacymod.php' => true));
+        $validator->assert_plugin_type('mod');
+        $validator->assert_moodle_version(0);
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'versionphpsyntax', '$module'));
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/nocomponent', array(
+            'baz/' => true,
+            'baz/version.php' => true,
+            'baz/lang/' => true,
+            'baz/lang/en/' => true,
+            'baz/lang/en/auth_baz.php' => true));
+        $validator->assert_plugin_type('auth');
+        $validator->assert_moodle_version(0);
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missingcomponent'));
+
         $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array(
             'foobar/' => true,
             'foobar/version.php' => true,
@@ -205,7 +227,7 @@ class tool_installaddon_validator_testcase extends basic_testcase {
         $this->assertTrue($this->has_message($validator->get_messages(), $validator::WARNING, 'multiplelangenfiles'));
         $this->assertTrue(is_null($validator->get_language_file_name()));
 
-        $validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array(
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/wronglang', array(
             'bah/' => true,
             'bah/version.php' => true,
             'bah/lang/' => true,
index 4c22571..00f0c61 100644 (file)
@@ -24,6 +24,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component  = 'tool_installaddon';
-$plugin->version    = 2015051100;
+$plugin->version    = 2015080601;
 $plugin->requires   = 2015050500;
 $plugin->maturity   = MATURITY_STABLE;
index 65a752d..a2ccf4e 100644 (file)
@@ -30,7 +30,7 @@ $string['langimportdisabled'] = 'Language import feature has been disabled. You
 $string['langpackinstalled'] = 'Language pack \'{$a}\' was successfully installed';
 $string['langpackinstalledevent'] = 'Language pack installed';
 $string['langpackremoved'] = 'Language pack \'{$a}\' was uninstalled';
-$string['langpacknotremoved'] = 'An error has occurred, language pack \'{$a}\' is not completely uninstalled, please check file permissions';
+$string['langpacknotremoved'] = 'An error has occurred; language pack \'{$a}\' is not completely uninstalled. Please check file permissions.';
 $string['langpackremovedevent'] = 'Language pack uninstalled';
 $string['langpackupdateskipped'] = 'Update of \'{$a}\' language pack skipped';
 $string['langpackuptodate'] = 'Language pack \'{$a}\' is up-to-date';
@@ -45,6 +45,6 @@ $string['pluginname'] = 'Language packs';
 $string['purgestringcaches'] = 'Purge string caches';
 $string['remotelangnotavailable'] = 'Because Moodle cannot connect to download.moodle.org, it is not possible for language packs to be installed automatically. Please download the appropriate ZIP file(s) from <a href="https://download.moodle.org/langpack/">download.moodle.org/langpack</a>, copy them to your {$a} directory and unzip them manually.';
 $string['selectlangs'] = 'Select languages to unistall!';
-$string['uninstall'] = 'Uninstall selected language packs';
+$string['uninstall'] = 'Uninstall selected language pack(s)';
 $string['uninstallconfirm'] = 'You are about to completely uninstall these language packs: <strong>{$a}</strong>. Are you sure?';
 $string['updatelangs'] = 'Update all installed language packs';
index ff21c8e..757d46e 100644 (file)
@@ -38,7 +38,7 @@ Feature: Manage language packs
     And I set the field "Available language packs" to "English - Pirate (en_ar)"
     And I press "Install selected language pack(s)"
     When I set the field "Installed language packs" to "English - Pirate (en_ar)"
-    And I press "Uninstall selected language pack"
+    And I press "Uninstall selected language pack(s)"
     And I press "Continue"
     Then I should see "Language pack 'en_ar' was uninstalled"
     And the "Installed language packs" select box should not contain "English - Pirate (en_ar)"
@@ -52,7 +52,7 @@ Feature: Manage language packs
     Given I log in as "admin"
     And I navigate to "Language packs" node in "Site administration > Language"
     When I set the field "Installed language packs" to "English (en)"
-    And I press "Uninstall selected language pack"
+    And I press "Uninstall selected language pack(s)"
     Then I should see "English language pack can not be uninstalled"
     And I navigate to "Live logs" node in "Site administration > Reports"
     And I should not see "Language pack uninstalled"
index 6904d48..659ee2e 100644 (file)
@@ -149,6 +149,14 @@ if ($execute = $options['execute']) {
         mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
         mtrace("... used " . (microtime(true) - $pretime) . " seconds");
         mtrace("Task failed: " . $e->getMessage());
+        if ($CFG->debugdeveloper) {
+            if (!empty($e->debuginfo)) {
+                mtrace("Debug info:");
+                mtrace($e->debuginfo);
+            }
+            mtrace("Backtrace:");
+            mtrace(format_backtrace($e->getTrace(), true));
+        }
         \core\task\manager::scheduled_task_failed($task);
         get_mailer('close');
         exit(1);
index eaf3f6c..22733b2 100644 (file)
@@ -187,7 +187,7 @@ class auth_plugin_cas extends auth_plugin_ldap {
         }
 
         // If Moodle is configured to use a proxy, phpCAS needs some curl options set.
-        if (!empty($CFG->proxyhost) && !is_proxybypass($this->config->hostname)) {
+        if (!empty($CFG->proxyhost) && !is_proxybypass(phpCAS::getServerLoginURL())) {
             phpCAS::setExtraCurlOption(CURLOPT_PROXY, $CFG->proxyhost);
             if (!empty($CFG->proxyport)) {
                 phpCAS::setExtraCurlOption(CURLOPT_PROXYPORT, $CFG->proxyport);
index c9f93b2..f0ed9b2 100644 (file)
@@ -31,7 +31,9 @@ class block_messages extends block_base {
         global $USER, $CFG, $DB, $OUTPUT;
 
         if (!$CFG->messaging) {
+            $this->content = new stdClass;
             $this->content->text = '';
+            $this->content->footer = '';
             if ($this->page->user_is_editing()) {
                 $this->content->text = get_string('disabled', 'message');
             }
index 4cdcdf7..d56c330 100644 (file)
@@ -41,9 +41,9 @@ $string['newspolitics'] = 'News &amp; Politics';
 $string['numberofvideos'] = 'Number of videos';
 $string['peopleblogs'] = 'People &amp; Blogs';
 $string['petsanimals'] = 'Pets &amp; Animals';
-$string['pluginname'] = 'Youtube';
+$string['pluginname'] = 'YouTube';
 $string['requesterror'] = 'Data could not be obtained from the server. Contact your administrator if the problem persist.';
 $string['scienceandtech'] = 'Science &amp; Tech';
 $string['sports'] = 'Sports';
-$string['tag_youtube:addinstance'] = 'Add a new youtube block';
+$string['tag_youtube:addinstance'] = 'Add a new YouTube block';
 $string['travel'] = 'Travel &amp; Places';
index 67146c0..95e1705 100644 (file)
@@ -4,17 +4,31 @@ Feature: Add and configure blocks throughout the site
   As a manager
   I need to set and configure blocks throughout the site
 
-  Scenario: Add and configure a block throughtout the site
+  Background:
     Given the following "courses" exist:
       | fullname | shortname | category |
       | Course 1 | C1 | 0 |
     And the following "users" exist:
       | username | firstname | lastname | email |
       | manager1 | Manager | 1 | manager1@example.com |
+      | teacher1 | teacher | 1 | teacher@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
     And the following "system role assigns" exist:
       | user | course | role |
       | manager1 | Acceptance test site | manager |
-    And I log in as "manager1"
+    # Allow at least one role assignment in the block context:
+    And I log in as "admin"
+    And I navigate to "Define roles" node in "Site administration > Users > Permissions"
+    And I follow "Edit Non-editing teacher role"
+    And I set the following fields to these values:
+      | Block | 1 |
+    And I press "Save changes"
+    And I log out
+
+  Scenario: Add and configure a block throughtout the site
+    Given I log in as "manager1"
     And I am on site homepage
     And I follow "Turn editing on"
     And I add the "Comments" block
@@ -35,25 +49,13 @@ Feature: Add and configure blocks throughout the site
     And I should see "Comments" in the "//*[@id='region-pre' or @id='block-region-side-pre']/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]" "xpath_element"
 
   Scenario: Blocks on the dashboard page cannot have roles assigned to them
-    Given the following "users" exist:
-      | username | firstname | lastname | email |
-      | manager1 | Manager | 1 | manager1@example.com |
-    And I log in as "manager1"
+    Given I log in as "manager1"
     And I click on "Dashboard" "link" in the "Navigation" "block"
     When I press "Customise this page"
     Then I should not see "Assign roles in Navigation block"
 
   Scenario: Blocks on courses can have roles assigned to them
-    Given the following "courses" exist:
-      | fullname | shortname | category |
-      | Course 1 | C1 | 0 |
-    And the following "users" exist:
-      | username | firstname | lastname | email               |
-      | teacher1 | teacher   | 1        | teacher@example.com |
-    And the following "course enrolments" exist:
-      | user     | course | role           |
-      | teacher1 | C1     | editingteacher |
-    And I log in as "teacher1"
+    Given I log in as "teacher1"
     And I follow "Course 1"
     And I follow "Turn editing on"
     Then I should see "Assign roles in Search forums block"
index 6196a57..df033d9 100644 (file)
@@ -489,8 +489,8 @@ $CFG->admin = 'admin';
 //      $CFG->supportuserid = -20;
 //
 // Moodle 2.7 introduces a locking api for critical tasks (e.g. cron).
-// The default locking system to use is DB locking for MySQL and Postgres, and File
-// locking for Oracle and SQLServer. If $CFG->preventfilelocking is set, then the default
+// The default locking system to use is DB locking for Postgres, and file locking for
+// MySQL, Oracle and SQLServer. If $CFG->preventfilelocking is set, then the default
 // will always be DB locking. It can be manually set to one of the lock
 // factory classes listed below, or one of your own custom classes implementing the
 // \core\lock\lock_factory interface.
index 31d9391..ec26762 100644 (file)
@@ -1671,9 +1671,10 @@ class core_course_renderer extends plugin_renderer_base {
         $site = get_site();
         $output = '';
 
-        if (can_edit_in_category($category)) {
+        if (can_edit_in_category($coursecat->id)) {
             // Add 'Manage' button if user has permissions to edit this category.
-            $managebutton = $this->single_button(new moodle_url('/course/management.php'), get_string('managecourses'), 'get');
+            $managebutton = $this->single_button(new moodle_url('/course/management.php',
+                array('categoryid' => $coursecat->id)), get_string('managecourses'), 'get');
             $this->page->set_button($managebutton);
         }
         if (!$coursecat->id) {
index c8e086a..6579845 100644 (file)
Binary files a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js and b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js differ
index 1704f91..3da12bd 100644 (file)
Binary files a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js and b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js differ
index c8e086a..6579845 100644 (file)
Binary files a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js and b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js differ
index c8683e3..c4af5f4 100644 (file)
@@ -293,7 +293,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
             };
             this.send_request(data);
             if (M.core.actionmenu && M.core.actionmenu.instance) {
-                M.core.actionmenu.instance.hideMenu();
+                M.core.actionmenu.instance.hideMenu(ev);
             }
 
         }, this);
@@ -540,7 +540,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
 
         this.send_request(data, null, function(response) {
             if (M.core.actionmenu && M.core.actionmenu.instance) {
-                M.core.actionmenu.instance.hideMenu();
+                M.core.actionmenu.instance.hideMenu(ev);
             }
 
             // Try to retrieve the existing string from the server
index 7aab283..dd62786 100644 (file)
@@ -192,9 +192,6 @@ class enrol_meta_observer extends enrol_meta_handler {
         }
 
         foreach ($enrols as $enrol) {
-            $enrol->customint = 0;
-            $DB->update_record('enrol', $enrol);
-
             if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND or $unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
                 // This makes all enrolments suspended very quickly.
                 $plugin->update_status($enrol, ENROL_INSTANCE_DISABLED);
index a351eaa..fbfe359 100644 (file)
@@ -2873,6 +2873,12 @@ abstract class grade_helper {
                 continue;
             }
 
+            // Singleview doesn't doesn't accomodate for all cap combos yet, so this is hardcoded..
+            if ($plugin === 'singleview' && !has_all_capabilities(array('moodle/grade:viewall',
+                    'moodle/grade:edit'), $context)) {
+                continue;
+            }
+
             $pluginstr = get_string('pluginname', 'gradereport_'.$plugin);
             $url = new moodle_url('/grade/report/'.$plugin.'/index.php', array('id'=>$courseid));
             $gradereports[$plugin] = new grade_plugin_info($plugin, $url, $pluginstr);
index 244af2f..88742ea 100644 (file)
@@ -590,7 +590,8 @@ class grade_report_grader extends grade_report {
 
         $showuserimage = $this->get_pref('showuserimage');
         $canseeuserreport = has_capability('gradereport/'.$CFG->grade_profilereport.':view', $this->context);
-        $canseesingleview = has_capability('gradereport/singleview:view', $this->context);
+        $canseesingleview = has_all_capabilities(array('gradereport/singleview:view', 'moodle/grade:viewall',
+            'moodle/grade:edit'), $this->context);
         $hasuserreportcell = $canseeuserreport || $canseesingleview;
 
         $strfeedback  = $this->get_lang_string("feedback");
@@ -836,7 +837,9 @@ class grade_report_grader extends grade_report {
                     }
 
                     $singleview = '';
-                    if (has_capability('gradereport/singleview:view', $this->context)) {
+                    if (has_all_capabilities(array('gradereport/singleview:view', 'moodle/grade:viewall',
+                        'moodle/grade:edit'), $this->context)) {
+
                         $url = new moodle_url('/grade/report/singleview/index.php', array(
                             'id' => $this->course->id,
                             'item' => 'grade',
index 5ecb430..cd4fead 100644 (file)
@@ -52,7 +52,7 @@ class gradereport_singleview extends grade_report {
      * @return array List of warnings
      */
     public function process_data($data) {
-        if (has_capability('moodle/grade:manage', $this->context)) {
+        if (has_capability('moodle/grade:edit', $this->context)) {
             return $this->screen->process($data);
         }
     }
index d6928fa..0f35f82 100644 (file)
@@ -731,7 +731,12 @@ class grade_report_user extends grade_report {
                 if ($gradecat->aggregation == GRADE_AGGREGATE_SUM) {
                     // Natural aggregation/Sum of grades does not consider the mingrade, cannot traditionnally normalise it.
                     $graderange = $this->aggregationhints[$itemid]['grademax'];
-                    $gradeval = $this->aggregationhints[$itemid]['grade'] / $graderange;
+
+                    if ($graderange != 0) {
+                        $gradeval = $this->aggregationhints[$itemid]['grade'] / $graderange;
+                    } else {
+                        $gradeval = 0;
+                    }
                 } else {
                     $gradeval = grade_grade::standardise_score($this->aggregationhints[$itemid]['grade'],
                         $this->aggregationhints[$itemid]['grademin'], $this->aggregationhints[$itemid]['grademax'], 0, 1);
index 06083dc..5662264 100644 (file)
@@ -84,7 +84,7 @@ Feature: We can change the grading type and maximum grade point values
     And I set the field "grade[modgrade_type]" to "Point"
     And I set the field "grade[modgrade_point]" to "20000"
     And I press "Save and display"
-    Then I should see "Invalid Grade Value. This must be an integer between 0 and 900"
+    Then I should see "Invalid Grade Value. This must be an integer between 1 and 900"
     And I press "Cancel"
 
   @javascript
@@ -104,5 +104,5 @@ Feature: We can change the grading type and maximum grade point values
     And I follow "Test Assignment 1"
     And I follow "Edit settings"
     And I press "Save and display"
-    Then I should see "Invalid Grade Value. This must be an integer between 0 and 100"
+    Then I should see "Invalid Grade Value. This must be an integer between 1 and 100"
     And I press "Cancel"
index 7e7f4c7..97bb9df 100644 (file)
@@ -900,7 +900,7 @@ $string['registermoodleorgli1'] = 'You are added to a low-volume mailing list fo
 $string['registermoodleorgli2'] = 'Statistics about your site will be added to the {$a} of the worldwide Moodle community.';
 $string['registerwithmoodleorg'] = 'Register your site';
 $string['registration'] = 'Registration';
-$string['registration_help'] = 'Registering your site with Moodle.org is recommended in order to receive security alert notifications, to contribute <a href="http://moodle.org/stats">Moodle usage statistics</a> and to be able to share courses on <a href="http://moodle.net/">Moodle.net</a>.';
+$string['registration_help'] = 'It is recommended that you register your site in order to receive security alerts and access to Moodle.net, our course sharing platform.';
 $string['registrationwarning'] = 'Your site is not yet registered.';
 $string['releasenoteslink'] = 'For information about this version of Moodle, please see the online <a target="_blank" href="{$a}">Release Notes</a>';
 $string['rememberusername'] = 'Remember username';
index 02676d6..1840aa1 100644 (file)
@@ -470,7 +470,7 @@ $string['minimum_show_help'] = 'Minimum grade is used in calculating grades and
 $string['missingitemtypeoreid'] = 'Array key (itemtype or eid) missing from 2nd param of grade_edit_tree_column_select::get_item_cell($item, $params)';
 $string['missingscale'] = 'Scale must be selected';
 $string['mode'] = 'Mode';
-$string['modgradeerrorbadpoint'] = 'Invalid Grade Value. This must be an integer between 0 and {$a}';
+$string['modgradeerrorbadpoint'] = 'Invalid Grade Value. This must be an integer between 1 and {$a}';
 $string['modgradeerrorbadscale'] = 'Invalid scale selected. Please make sure you select a scale from the selections below.';
 $string['modgrade'] = 'Grade';
 $string['modgrade_help'] = 'Select the type of grading used for this activity. If "scale" is chosen, you can then choose the scale from the "scale" dropdown. If using "point" grading, you can then enter the maximum grade available for this activity.';
index 02d240b..3b5d5c8 100644 (file)
@@ -95,7 +95,7 @@ $string['errorcoursepublish'] = 'An error occurred during the course publication
 $string['errorcoursewronglypublished'] = 'A publication error has been returned by the hub. Please try again later.';
 $string['errorcron'] = 'An error occurred during registration update on "{$a->hubname}" ({$a->errormessage})';
 $string['errorcronnoxmlrpc'] = 'XML-RPC must be enabled in order to update the registration.';
-$string['errorhublisting'] = 'An error occurred when retrieving the hub listing from Moodle.org, please try again later. ({$a})';
+$string['errorhublisting'] = 'An error occurred when retrieving the hub listing from Moodle. Please try again later. ({$a})';
 $string['errorlangnotrecognized'] = 'The provided language code is unknown by Moodle. Please contact {$a}';
 $string['errorregistration'] = 'An error occurred during registration, please try again later. ({$a})';
 $string['errorunpublishcourses']= 'Due to an unexpected error, the courses could not be deleted on the hub. Try again later (recommended) or contact the hub administrator.';
@@ -118,7 +118,7 @@ $string['licence_help'] = 'Select the licence you want to distribute your course
 $string['licence_link'] = 'licenses';
 $string['logourl'] = 'Logo URL';
 $string['modulenumberaverage'] = 'Average number of course modules ({$a})';
-$string['moodleorg'] = 'Moodle.org';
+$string['moodleorg'] = 'Moodle';
 $string['mustselectsubject'] = 'You must select a subject';
 $string['name'] = 'Name';
 $string['name_help'] = 'This name will be showing in the course listing.';
@@ -158,9 +158,9 @@ $string['questionsnumber'] = 'Number of questions ({$a})';
 $string['registeredcourses'] = 'Registered courses';
 $string['registeredsites'] = 'Registered sites';
 $string['registrationinfo'] = 'Registration information';
-$string['registeredmoodleorg'] = 'Moodle.org ({$a})';
+$string['registeredmoodleorg'] = 'Moodle ({$a})';
 $string['registeredon'] = 'Where your site is registered';
-$string['registermoochtips'] = 'In order to register with Moodle.net, your site must be registered with Moodle.org.';
+$string['registermoochtips'] = 'Register your site with Moodle to get security alerts and access to Moodle.net, our course sharing platform.';
 $string['registersite'] = 'Register with {$a}';
 $string['registerwith'] = 'Register with a hub';
 $string['registrationconfirmed'] = 'Site registration confirmed';
index 15fd175..5ad5738 100644 (file)
@@ -38,7 +38,7 @@ $string['mediasettings'] = 'Media embedding';
 $string['mp3audio'] = 'MP3 audio';
 $string['mp3audio_desc'] = 'Files with extension *.mp3. Plays audio using Flowplayer, requires Flash plugin.';
 $string['legacyquicktime'] = 'QuickTime player';
-$string['legacyquicktime_desc'] = 'Files with extension *.mov, *.mp4, *.m4a, *.mp4 and *.mpg. Requires QuickTime player or codecs.';
+$string['legacyquicktime_desc'] = 'Files with extension *.mov, *.mp4, *.m4a and *.mpg. Requires QuickTime player or codecs.';
 $string['legacyreal'] = 'Real media player';
 $string['legacyreal_desc'] = 'Files with extension *.rm, *.ra, *.ram, *.rp and *.rv. Requires RealPlayer.';
 $string['legacywmp'] = 'Windows media player';
index dacd551..f9abcd8 100644 (file)
@@ -52,7 +52,7 @@ $string['emailtagline'] = 'This is a copy of a message sent to you at "{$a->site
 $string['emptysearchstring'] = 'You must search for something';
 $string['enabled'] = 'Enabled';
 $string['errorcallingprocessor'] = 'Error calling defined output';
-$string['errorwhilesendingmessage'] = 'An error occured while sending the message, please try again later.';
+$string['errorwhilesendingmessage'] = 'An error occurred while sending the message; please try again later.';
 $string['errortranslatingdefault'] = 'Error translating default setting provided by plugin, using system defaults instead.';
 $string['eventmessagecontactadded'] = 'Message contact added';
 $string['eventmessagecontactblocked'] = 'Message contact blocked';
index 78468c7..4a6e3fc 100644 (file)
@@ -1910,7 +1910,6 @@ $string['uploadserverlimit'] = 'Uploaded file exceeded the maximum size limit se
 $string['uploadthisfile'] = 'Upload this file';
 $string['url'] = 'URL';
 $string['used'] = 'Used';
-$string['userdetails'] = 'User details';
 $string['usedinnplaces'] = 'Used in {$a} places';
 $string['usemessageform'] = 'or use the form below to send a message to the selected students';
 $string['user'] = 'User';
index 126fb08..2ce0f2c 100644 (file)
@@ -195,6 +195,7 @@ $string['lasttry'] = 'Last try';
 $string['linkedfiledoesntexist'] = 'Linked file {$a} doesn\'t exist';
 $string['makechildof'] = 'Make child of \'{$a}\'';
 $string['maketoplevelitem'] = 'Move to top level';
+$string['manualgradeinvalidformat'] = 'That is not a valid number.';
 $string['matchgrades'] = 'Match grades';
 $string['matchgradeserror'] = 'Error if grade not listed';
 $string['matchgradesnearest'] = 'Nearest grade if not listed';
index c3e2378..544229a 100644 (file)
@@ -107,13 +107,13 @@ $string['cohort:view'] = 'View site-wide cohorts';
 $string['cohort:manage'] = 'Create, delete and move cohorts';
 $string['comment:delete'] = 'Delete comments';
 $string['comment:post'] = 'Post comments';
-$string['comment:view'] = 'Read comments';
+$string['comment:view'] = 'View comments';
 $string['community:add'] = 'Use the community block to search hubs and find courses';
 $string['community:download'] = 'Download a course from the community block';
 $string['confirmaddadmin'] = 'Do you really want to add user <strong>{$a}</strong> as new site administrator?';
 $string['confirmdeladmin'] = 'Do you really want to remove user <strong>{$a}</strong> from the list of site administrators?';
-$string['confirmroleprevent'] = 'Do you really want to remove <strong>{$a->role}</strong> from the list of allowed roles for capability {$a->cap} in context {$a->context}?';
-$string['confirmroleunprohibit'] = 'Do you really want to remove <strong>{$a->role}</strong> from the list of prohibited roles for capability {$a->cap} in context {$a->context}?';
+$string['confirmroleprevent'] = 'Do you really want to remove <strong>"{$a->role}"</strong> from the list of allowed roles for capability "{$a->cap}" in context "{$a->context}"?';
+$string['confirmroleunprohibit'] = 'Do you really want to remove <strong>"{$a->role}"</strong> from the list of prohibited roles for capability "{$a->cap}" in context "{$a->context}"?';
 $string['confirmunassign'] = 'Are you sure you wish to remove this role from this user?';
 $string['confirmunassigntitle'] = 'Confirm role change';
 $string['confirmunassignyes'] = 'Remove';
@@ -257,7 +257,7 @@ $string['noneinthisxmatching'] = 'No users matching \'{$a->search}\' in this {$a
 $string['norole'] = 'No role';
 $string['noroles'] = 'No roles';
 $string['noroleassignments'] = 'This user does not have any role assignments anywhere in this site.';
-$string['notabletoassignroleshere'] = 'You are not able to assign any roles here';
+$string['notabletoassignroleshere'] = 'Assigning of roles in this context has not been enabled by an administrator.';
 $string['notabletooverrideroleshere'] = 'You are not able to override the permissions on any roles here';
 $string['notes:manage'] = 'Manage notes';
 $string['notes:view'] = 'View notes';
@@ -321,7 +321,7 @@ $string['restore:userinfo'] = 'Restore user data';
 $string['restore:viewautomatedfilearea'] = 'Restore courses from automated backups';
 $string['risks'] = 'Risks';
 $string['roleallowheader'] = 'Allow role:';
-$string['roleallowinfo'] = 'Select a role to be added to the list of allowed roles in context {$a->context}, capability {$a->cap}:';
+$string['roleallowinfo'] = 'Select a role to be added to the list of allowed roles in context "{$a->context}", capability "{$a->cap}":';
 $string['role:assign'] = 'Assign roles to users';
 $string['roleassignments'] = 'Role assignments';
 $string['roledefinitions'] = 'Role definitions';
@@ -331,7 +331,7 @@ $string['role:manage'] = 'Create and manage roles';
 $string['role:override'] = 'Override permissions for others';
 $string['role:review'] = 'Review permissions for others';
 $string['roleprohibitheader'] = 'Prohibit role';
-$string['roleprohibitinfo'] = 'Select a role to be added to the list of prohibited roles in context {$a->context}, capability {$a->cap}:';
+$string['roleprohibitinfo'] = 'Select a role to be added to the list of prohibited roles in context "{$a->context}", capability "{$a->cap}":';
 $string['rolerisks'] = 'Role risks';
 $string['roles'] = 'Roles';
 $string['roles_help'] = 'A role is a collection of permissions defined for the whole system that you can assign to specific users in specific contexts.';
index e9360f0..3563fde 100644 (file)
@@ -2319,6 +2319,7 @@ class admin_setting_confightmleditor extends admin_setting_configtext {
         }
 
         $editor = editors_get_preferred_editor(FORMAT_HTML);
+        $editor->set_text($data);
         $editor->use_editor($this->get_id(), array('noclean'=>true));
 
         return format_admin_setting($this, $this->visiblename,
index 1cfd346..83625f7 100644 (file)
@@ -63,13 +63,12 @@ switch ($pagetype[0]) {
         $PAGE->set_blocks_editing_capability('moodle/my:manageblocks');
         break;
     case 'user':
-        if ($pagelayout == 'mydashboard') {
-            // If it's not the current user's profile, we need a different capability.
-            if ($PAGE->context->contextlevel == CONTEXT_USER && $PAGE->context->instanceid != $USER->id) {
-                $PAGE->set_blocks_editing_capability('moodle/user:manageblocks');
-            } else {
-                $PAGE->set_blocks_editing_capability('moodle/user:manageownblocks');
-            }
+        if ($pagetype[1] === 'profile' && $PAGE->context->contextlevel == CONTEXT_USER
+                && $PAGE->context->instanceid == $USER->id) {
+            // A user can only move blocks on their own site profile.
+            $PAGE->set_blocks_editing_capability('moodle/user:manageownblocks');
+        } else {
+            $PAGE->set_blocks_editing_capability('moodle/user:manageblocks');
         }
         break;
 }
diff --git a/lib/amd/build/permissionmanager.min.js b/lib/amd/build/permissionmanager.min.js
new file mode 100644 (file)
index 0000000..9f49f4a
Binary files /dev/null and b/lib/amd/build/permissionmanager.min.js differ
index 62d34b1..ed4aa32 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/permissionmanager.js b/lib/amd/src/permissionmanager.js
new file mode 100644 (file)
index 0000000..267f6cb
--- /dev/null
@@ -0,0 +1,260 @@
+// 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/>.
+/*
+ * @package    core
+ * @class      permissionmanager
+ * @copyright  2015 Martin Mastny <mastnym@vscht.cz>
+ * @since      3.0
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+ /**
+  * @module admin/permissionmanager
+  */
+define(['jquery', 'core/config','core/notification', 'core/templates'], function($, config, notification, templates) {
+
+     /**
+      * Used CSS selectors
+      * @access private
+      */
+    var SELECTORS = {
+        ADDROLE: 'a.allowlink, a.prohibitlink',
+        REMOVEROLE: 'a.preventlink, a.unprohibitlink',
+        UNPROHIBIT: 'a.unprohibitlink'
+        };
+    var rolesloadedevent = $.Event('rolesloaded');
+    var contextid;
+    var contextname;
+    var adminurl;
+    var overideableroles;
+    var panel = null;
+
+    /**
+     * Load all possible roles, which could be assigned from server
+     *
+     * @access private
+     * @method loadOverideableRoles
+     */
+    var loadOverideableRoles = function() {
+        var params = {
+            contextid: contextid,
+            getroles: 1,
+            sesskey: config.sesskey
+        };
+
+        $.post(adminurl + 'roles/ajax.php', params, function() {})
+            .done(function(data) {
+              try {
+                  overideableroles = data;
+                  loadOverideableRoles = function() {
+                      $('body').trigger(rolesloadedevent);
+                  };
+                  loadOverideableRoles();
+              }
+              catch(err) {
+                  notification.exception(err);
+              }
+            })
+            .fail(function(jqXHR, status, error) {
+                notification.exception(error);
+            });
+    };
+
+    /**
+     * Perform the UI changes after server change
+     *
+     * @access private
+     * @method changePermissions
+     * @param {jquery node} row
+     * @param {int} roleid
+     * @param {string} action
+     */
+    var changePermissions = function(row, roleid, action) {
+        var params = {
+            contextid: contextid,
+            roleid: roleid,
+            sesskey: M.cfg.sesskey,
+            action: action,
+            capability: row.data('name')
+        };
+        $.post(adminurl + 'roles/ajax.php', params, function() {})
+        .done(function(data) {
+            var action = data;
+            try {
+                var templatedata = {rolename: overideableroles[roleid],
+                                    roleid: roleid,
+                                    adminurl: adminurl,
+                                    imageurl: M.util.image_url('t/delete', 'moodle')
+                                    };
+                switch (action) {
+                    case 'allow':
+                        templatedata.spanclass = 'allowed';
+                        templatedata.linkclass = 'preventlink';
+                        templatedata.action = 'prevent';
+                        break;
+                    case 'prohibit':
+                        templatedata.spanclass = 'forbidden';
+                        templatedata.linkclass = 'unprohibitlink';
+                        templatedata.action = 'unprohibit';
+                        break;
+                    case 'prevent':
+                        row.find('a[data-role-id="' + roleid + '"]').first().closest('.allowed').remove();
+                        return;
+                    case 'unprohibit':
+                        row.find('a[data-role-id="' + roleid + '"]').first().closest('.forbidden').remove();
+                        return;
+                    default:
+                        return;
+                }
+                templates.render('core/permissionmanager_role',templatedata)
+                .done(function (content) {
+                    if (action == 'allow'){
+                        $(content).insertBefore(row.find('.allowmore:first'));
+                    }else if (action == 'prohibit'){
+                        $(content).insertBefore(row.find('.prohibitmore:first'));
+                        // Remove allowed link
+                        var allowedLink = row.find('.allowedroles').first().find('a[data-role-id="' + roleid + '"]');
+                        if (allowedLink) {
+                            allowedLink.first().closest('.allowed').remove();
+                        }
+                    }
+                    panel.hide();
+                })
+                .fail(notification.exception);
+            }
+            catch(err) {
+                notification.exception(err);
+            }
+        })
+        .fail(function(jqXHR, status, error) {
+            notification.exception(error);
+        });
+    };
+
+    /**
+     * Prompts user for selecting a role which is permitted
+     *
+     * @access private
+     * @method handleAddRole
+     * @param {event} e
+     */
+    var handleAddRole = function(e){
+        e.preventDefault();
+
+        $('body').one('rolesloaded', function() {
+            var link = $(e.currentTarget);
+            var action = link.data('action');
+            var row = link.closest('tr.rolecap');
+            var confirmationDetails = {
+                cap: row.data('humanname'),
+                context: contextname
+            };
+            var message = M.util.get_string('role' + action + 'info', 'core_role', confirmationDetails);
+            if (panel === null){
+                panel = new M.core.dialogue ({
+                    draggable: true,
+                    modal: true,
+                    closeButton: true,
+                    width: '450px'
+                });
+            }
+            panel.set('headerContent', M.util.get_string('role' + action + 'header', 'core_role'));
+
+            var i, existingrolelinks;
+
+            var roles = [];
+            switch (action){
+                case 'allow':
+                    existingrolelinks = row.find(SELECTORS.REMOVEROLE);
+                    break;
+                case 'prohibit':
+                    existingrolelinks = row.find(SELECTORS.UNPROHIBIT);
+                    break;
+            }
+            for (i in overideableroles) {
+                var disabled = '';
+                var disable = existingrolelinks.filter("[data-role-id='" + i + "']").length;
+                if (disable){
+                    disabled = 'disabled';
+                }
+                var roledetails = {roleid:i, rolename: overideableroles[i], disabled:disabled};
+                roles.push(roledetails);
+            }
+
+            templates.render('core/permissionmanager_panelcontent',{message:message, roles:roles})
+            .done(function (content) {
+                panel.set('bodyContent', content);
+                panel.show();
+                $('div.role_buttons').delegate('input', 'click',function(e){
+                    var roleid = $(e.currentTarget).data('role-id');
+                    changePermissions(row, roleid, action);
+                });
+            })
+            .fail(notification.exception);
+
+        });
+        loadOverideableRoles();
+    };
+
+    /**
+     * Prompts user when removing permission
+     *
+     * @access private
+     * @method handleRemoveRole
+     * @param {event} e
+     */
+    var handleRemoveRole = function(e){
+        e.preventDefault();
+        $('body').one('rolesloaded', function() {
+            var link = $(e.currentTarget);
+            var action = link.data('action');
+            var roleid = link.data('role-id');
+            var row = link.closest('tr.rolecap');
+            var questionDetails = {
+                role: overideableroles[roleid],
+                cap: row.data('humanname'),
+                context: contextname
+            };
+
+            notification.confirm(M.util.get_string('confirmunassigntitle', 'core_role'),
+                M.util.get_string('confirmrole' + action, 'core_role',questionDetails),
+                M.util.get_string('confirmunassignyes', 'core_role'),
+                M.util.get_string('confirmunassignno', 'core_role'),
+                function(){
+                   changePermissions(row, roleid, action);
+                }
+            );
+         });
+        loadOverideableRoles();
+    };
+
+    return /** @alias module:core/permissionmanager */ {
+        /**
+         * Initialize permissionmanager
+         * @access public
+         * @param {int} contextid
+         * @param {string} contextname
+         * @param {string} adminurl
+         */
+        initialize : function(args) {
+            contextid = args.contextid;
+            contextname = args.contextname;
+            adminurl = args.adminurl;
+            var body = $('body');
+            body.delegate(SELECTORS.ADDROLE, 'click', handleAddRole);
+            body.delegate(SELECTORS.REMOVEROLE, 'click', handleRemoveRole);
+        }
+    };
+});
\ No newline at end of file
index d03738d..93ed7d1 100644 (file)
@@ -298,6 +298,7 @@ define([ 'core/mustache',
 
         if (cached) {
             deferred.resolve(cached);
+            templateCache[searchKey] = cached;
             return deferred.promise();
         }
 
index 5d1874e..4866dc2 100644 (file)
@@ -1092,24 +1092,39 @@ class block_manager {
             $controls[] = new action_menu_link_secondary($url, $icon, $str, $attributes);
         }
 
-        // Assign roles icon.
-        if ($this->page->pagetype != 'my-index' && has_capability('moodle/role:assign', $block->context)) {
-            //TODO: please note it is sloppy to pass urls through page parameters!!
-            //      it is shortened because some web servers (e.g. IIS by default) give
-            //      a 'security' error if you try to pass a full URL as a GET parameter in another URL.
-            $return = $this->page->url->out(false);
-            $return = str_replace($CFG->wwwroot . '/', '', $return);
-
-            $rolesurl = new moodle_url('/admin/roles/assign.php', array('contextid'=>$block->context->id,
-                                                                         'returnurl'=>$return));
-            // Delete icon.
-            $str = new lang_string('assignrolesinblock', 'block', $blocktitle);
-            $controls[] = new action_menu_link_secondary(
-                $rolesurl,
-                new pix_icon('t/assignroles', $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                $str,
-                array('class' => 'editing_roles')
-            );
+        // Display either "Assign roles" or "Permissions" or "Change permissions" icon (whichever first is available).
+        if ($this->page->pagetype != 'my-index') {
+            $rolesurl = null;
+
+            if (get_assignable_roles($block->context, ROLENAME_SHORT)) {
+                $rolesurl = new moodle_url('/admin/roles/assign.php', array('contextid' => $block->context->id));
+                $str = new lang_string('assignrolesinblock', 'block', $blocktitle);
+                $icon = 'i/assignroles';
+            } else if (has_capability('moodle/role:review', $block->context) or get_overridable_roles($block->context)) {
+                $rolesurl = new moodle_url('/admin/roles/permissions.php', array('contextid' => $block->context->id));
+                $str = get_string('permissions', 'role');
+                $icon = 'i/permissions';
+            } else if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override', 'moodle/role:assign'), $block->context)) {
+                $rolesurl = new moodle_url('/admin/roles/check.php', array('contextid' => $block->context->id));
+                $str = get_string('checkpermissions', 'role');
+                $icon = 'i/checkpermissions';
+            }
+
+            if ($rolesurl) {
+                //TODO: please note it is sloppy to pass urls through page parameters!!
+                //      it is shortened because some web servers (e.g. IIS by default) give
+                //      a 'security' error if you try to pass a full URL as a GET parameter in another URL.
+                $return = $this->page->url->out(false);
+                $return = str_replace($CFG->wwwroot . '/', '', $return);
+                $rolesurl->param('returnurl', $return);
+
+                $controls[] = new action_menu_link_secondary(
+                    $rolesurl,
+                    new pix_icon($icon, $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+                    $str,
+                    array('class' => 'editing_roles')
+                );
+            }
         }
 
         if ($this->user_can_delete_block($block)) {
index 99d17f4..ddd18d4 100644 (file)
@@ -288,15 +288,30 @@ class core_plugin_manager {
         foreach ($plugintypes as $type => $typedir) {
             $plugs = core_component::get_plugin_list($type);
             foreach ($plugs as $plug => $fullplug) {
+                $module = new stdClass();
                 $plugin = new stdClass();
                 $plugin->version = null;
-                $module = $plugin;
                 include($fullplug.'/version.php');
+
+                // Check if the legacy $module syntax is still used.
+                if (!is_object($module) or (count((array)$module) > 0)) {
+                    debugging('Unsupported $module syntax detected in version.php of the '.$type.'_'.$plug.' plugin.');
+                    $skipcache = true;
+                }
+
+                // Check if the component is properly declared.
+                if (empty($plugin->component) or ($plugin->component !== $type.'_'.$plug)) {
+                    debugging('Plugin '.$type.'_'.$plug.' does not declare valid $plugin->component in its version.php.');
+                    $skipcache = true;
+                }
+
                 $this->presentplugins[$type][$plug] = $plugin;
             }
         }
 
-        $cache->set('present', $this->presentplugins);
+        if (empty($skipcache)) {
+            $cache->set('present', $this->presentplugins);
+        }
     }
 
     /**
index b1a5e16..d565a22 100644 (file)
@@ -377,7 +377,7 @@ class manager {
         $user = null;
 
         if (!empty($CFG->opentogoogle)) {
-            if (is_web_crawler()) {
+            if (\core_useragent::is_web_crawler()) {
                 $user = guest_user();
             }
             $referer = get_local_referer(false);
index 29f1a07..fcc3c6d 100644 (file)
@@ -71,7 +71,7 @@ class core_useragent {
         self::DEVICETYPE_DEFAULT,
         self::DEVICETYPE_LEGACY,
         self::DEVICETYPE_MOBILE,
-        self::DEVICETYPE_TABLET
+        self::DEVICETYPE_TABLET,
     );
 
     /**
@@ -201,6 +201,7 @@ class core_useragent {
 
     /**
      * Returns true if the user appears to be on a tablet.
+     *
      * @return int
      */
     protected function is_useragent_tablet() {
@@ -208,6 +209,16 @@ class core_useragent {
         return (preg_match($tabletregex, $this->useragent));
     }
 
+    /**
+     * Whether the user agent relates to a web crawler.
+     * This includes all types of web crawler.
+     * @return bool
+     */
+    protected function is_useragent_web_crawler() {
+        $regex = '/Googlebot|google\.com|Yahoo! Slurp|\[ZSEBOT\]|msnbot|bingbot|BingPreview|Yandex|AltaVista|Baiduspider|Teoma/';
+        return (preg_match($regex, $this->useragent));
+    }
+
     /**
      * Gets a list of known device types.
      *
@@ -926,4 +937,15 @@ class core_useragent {
         // This browser does not support json.
         return false;
     }
+
+    /**
+     * Returns true if the client appears to be some kind of web crawler.
+     * This may include other types of crawler.
+     *
+     * @return bool
+     */
+    public static function is_web_crawler() {
+        $instance = self::instance();
+        return (bool) $instance->is_useragent_web_crawler();
+    }
 }
index 5db8ba8..e6e6b46 100644 (file)
@@ -91,6 +91,14 @@ function cron_run() {
                 mtrace("... used " . (microtime(1) - $pretime) . " seconds");
             }
             mtrace("Scheduled task failed: " . $task->get_name() . "," . $e->getMessage());
+            if ($CFG->debugdeveloper) {
+                 if (!empty($e->debuginfo)) {
+                    mtrace("Debug info:");
+                    mtrace($e->debuginfo);
+                }
+                mtrace("Backtrace:");
+                mtrace(format_backtrace($e->getTrace(), true));
+            }
             \core\task\manager::scheduled_task_failed($task);
         }
         get_mailer('close');
@@ -127,6 +135,14 @@ function cron_run() {
                 mtrace("... used " . (microtime(1) - $pretime) . " seconds");
             }
             mtrace("Adhoc task failed: " . get_class($task) . "," . $e->getMessage());
+            if ($CFG->debugdeveloper) {
+                 if (!empty($e->debuginfo)) {
+                    mtrace("Debug info:");
+                    mtrace($e->debuginfo);
+                }
+                mtrace("Backtrace:");
+                mtrace(format_backtrace($e->getTrace(), true));
+            }
             \core\task\manager::adhoc_task_failed($task);
         }
         get_mailer('close');
index fcc226d..b077fb3 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20150608" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20150824" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <INDEX NAME="courseid-name" UNIQUE="true" FIELDS="courseid, name"/>
       </INDEXES>
     </TABLE>
-    <TABLE NAME="webdav_locks" COMMENT="Resource locks for WebDAV users">
-      <FIELDS>
-        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
-        <FIELD NAME="token" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="path" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="expiry" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="recursive" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="exclusivelock" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="created" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="modified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="owner" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
-      </FIELDS>
-      <KEYS>
-        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
-        <KEY NAME="token" TYPE="unique" FIELDS="token"/>
-      </KEYS>
-      <INDEXES>
-        <INDEX NAME="path" UNIQUE="false" FIELDS="path"/>
-        <INDEX NAME="expiry" UNIQUE="false" FIELDS="expiry"/>
-      </INDEXES>
-    </TABLE>
     <TABLE NAME="portfolio_instance" COMMENT="base table (not including config data) for instances of portfolio plugins.">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
index 0adf125..9dcce10 100644 (file)
@@ -4456,5 +4456,19 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2015081300.01);
     }
 
+    if ($oldversion < 2015082400.00) {
+
+        // Define table webdav_locks to be dropped.
+        $table = new xmldb_table('webdav_locks');
+
+        // Conditionally launch drop table for webdav_locks.
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2015082400.00);
+    }
+
     return true;
 }
index 8c85c9d..82eab5a 100644 (file)
@@ -1003,6 +1003,7 @@ function print_textarea($unused, $rows, $cols, $width, $height, $name, $value=''
 
     editors_head_setup();
     $editor = editors_get_preferred_editor(FORMAT_HTML);
+    $editor->set_text($value);
     $editor->use_editor($id, array('legacy'=>true));
 
     $str .= "\n".'<textarea class="form-textarea" id="'. $id .'" name="'. $name .'" rows="'. $rows .'" cols="'. $cols .'" spellcheck="true">'."\n";
@@ -1151,7 +1152,7 @@ function navmenu($course, $cm=NULL, $targetwindow='self') {
 /**
  * @deprecated please use calendar_event::create() instead.
  */
- function add_event($event) {
+function add_event($event) {
     throw new coding_exception('add_event() can not be used any more, please use calendar_event::create() instead.');
 }
 
@@ -2378,4 +2379,22 @@ function get_referer($stripquery = true) {
     } else {
         return '';
     }
-}
\ No newline at end of file
+}
+
+/**
+ * Checks if current user is a web crawler.
+ *
+ * This list can not be made complete, this is not a security
+ * restriction, we make the list only to help these sites
+ * especially when automatic guest login is disabled.
+ *
+ * If admin needs security they should enable forcelogin
+ * and disable guest access!!
+ *
+ * @return bool
+ * @deprecated since Moodle 3.0 use \core_useragent::is_web_crawler instead.
+ */
+function is_web_crawler() {
+    debugging("is_web_crawler() has been deprecated, please use \\core_useragent\\is_web_crawler() instead.", DEBUG_DEVELOPER);
+    return core_useragent::is_crawler();
+}
index fa45fd4..6591531 100644 (file)
@@ -25,6 +25,7 @@
 define('AJAX_SCRIPT', true);
 
 require_once(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir . '/filestorage/file_storage.php');
 
 $contextid = required_param('contextid', PARAM_INT);
 $elementid = required_param('elementid', PARAM_ALPHANUMEXT);
@@ -114,34 +115,49 @@ if ($action === 'save') {
         $stale = $record->timemodified < $before;
         require_once($CFG->libdir . '/filelib.php');
 
-        // This function copies all the files in one draft area, to another area (in this case it's
-        // another draft area). It also rewrites the text to @@PLUGINFILE@@ links.
-        $newdrafttext = file_save_draft_area_files($record->draftid,
-                                                   $usercontext->id,
-                                                   'user',
-                                                   'draft',
-                                                   $newdraftid,
-                                                   array(),
-                                                   $record->drafttext);
-
-        // Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again).
-        $newdrafttext = file_rewrite_pluginfile_urls($newdrafttext,
-                                                     'draftfile.php',
-                                                     $usercontext->id,
-                                                     'user',
-                                                     'draft',
-                                                     $newdraftid);
-        $record->drafttext = $newdrafttext;
+        $fs = get_file_storage();
+        $files = $fs->get_directory_files($usercontext->id, 'user', 'draft', $newdraftid, '/', true, true);
 
-        $record->pageinstance = $pageinstance;
-        $record->draftid = $newdraftid;
-        $record->timemodified = time();
-        $DB->update_record('editor_atto_autosave', $record);
+        $lastfilemodified = 0;
+        foreach ($files as $file) {
+            $lastfilemodified = max($lastfilemodified, $file->get_timemodified());
+        }
+        if ($record->timemodified < $lastfilemodified) {
+            $stale = true;
+        }
 
-        // A response means the draft has been restored and here is the auto-saved text.
         if (!$stale) {
+            // This function copies all the files in one draft area, to another area (in this case it's
+            // another draft area). It also rewrites the text to @@PLUGINFILE@@ links.
+            $newdrafttext = file_save_draft_area_files($record->draftid,
+                                                       $usercontext->id,
+                                                       'user',
+                                                       'draft',
+                                                       $newdraftid,
+                                                       array(),
+                                                       $record->drafttext);
+
+            // Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again).
+            $newdrafttext = file_rewrite_pluginfile_urls($newdrafttext,
+                                                         'draftfile.php',
+                                                         $usercontext->id,
+                                                         'user',
+                                                         'draft',
+                                                         $newdraftid);
+            $record->drafttext = $newdrafttext;
+
+            $record->pageinstance = $pageinstance;
+            $record->draftid = $newdraftid;
+            $record->timemodified = time();
+            $DB->update_record('editor_atto_autosave', $record);
+
+            // A response means the draft has been restored and here is the auto-saved text.
             $response['result'] = $record->drafttext;
             echo json_encode($response);
+        } else {
+            $DB->delete_records('editor_atto_autosave', array('id' => $record->id));
+
+            // No response means no error.
         }
         die();
     }
diff --git a/lib/editor/atto/classes/task/autosave_cleanup_task.php b/lib/editor/atto/classes/task/autosave_cleanup_task.php
new file mode 100644 (file)
index 0000000..f7ab918
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A scheduled task.
+ *
+ * @package    editor_atto
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace editor_atto\task;
+
+use \core\task\scheduled_task;
+
+/**
+ * Simple task to run the autosave cleanup task.
+ */
+class autosave_cleanup_task extends scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('taskautosavecleanup', 'editor_atto');
+    }
+
+    /**
+     * Do the job.
+     * Throw exceptions on errors (the job will be retried).
+     */
+    public function execute() {
+        global $DB;
+
+        $now = time();
+        // This is the oldest time any autosave text will be recovered from.
+        // This is so that there is a good chance the draft files will still exist (there are many variables so
+        // this is impossible to guarantee).
+        $before = $now - 60*60*24*4;
+
+        $DB->delete_records_select('editor_atto_autosave', 'timemodified < :before', array('before' => $before));
+    }
+
+}
diff --git a/lib/editor/atto/db/tasks.php b/lib/editor/atto/db/tasks.php
new file mode 100644 (file)
index 0000000..9aba0ee
--- /dev/null
@@ -0,0 +1,44 @@
+<?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/>.
+
+/**
+ * Definition of core scheduled tasks.
+ *
+ * The handlers defined on this file are processed and registered into
+ * the Moodle DB after any install or upgrade operation. All plugins
+ * support this.
+ *
+ * @package   core
+ * @category  task
+ * @copyright 2013 Damyon Wiese
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/* List of handlers */
+
+$tasks = array(
+    array(
+        'classname' => 'editor_atto\task\autosave_cleanup_task',
+        'blocking' => 0,
+        'minute' => 'R',
+        'hour' => 'R',
+        'day' => '*',
+        'dayofweek' => 'R',
+        'month' => '*'
+    )
+);
index ebbca5f..ed7f087 100644 (file)
@@ -36,6 +36,7 @@ $string['pluginname'] = 'Atto HTML editor';
 $string['subplugintype_atto'] = 'Atto plugin';
 $string['subplugintype_atto_plural'] = 'Atto plugins';
 $string['settings'] = 'Atto toolbar settings';
+$string['taskautosavecleanup'] = 'Delete expired autosave drafts from the database.';
 $string['textrecovered'] = 'A draft version of this text was automatically restored.';
 $string['toolbarconfig'] = 'Toolbar config';
 $string['toolbarconfig_desc'] = 'The list of plugins and the order they are displayed can be configured here. The configuration consists of groups (one per line) followed by the ordered list of plugins for that group. The group is separated from the plugins with an equals sign and the plugins are separated with commas. The group names must be unique and should indicate what the buttons have in common. Button and group names should not be repeated and may only contain alphanumeric characters.';
index 65caee5..a450917 100644 (file)
@@ -161,6 +161,8 @@ class atto_texteditor extends texteditor {
         }
         $contentcss     = $PAGE->theme->editor_css_url()->out(false);
 
+        // Note <> is a safe separator, because it will not appear in the output of s().
+        $pagehash = sha1($PAGE->url . '<>' . s($this->get_text()));
         $params = array(
             'elementid' => $elementid,
             'content_css' => $contentcss,
@@ -171,7 +173,7 @@ class atto_texteditor extends texteditor {
             'directionality' => $directionality,
             'filepickeroptions' => array(),
             'plugins' => $plugins,
-            'pageHash' => sha1($PAGE->url)
+            'pageHash' => $pagehash,
         );
         if ($fpoptions) {
             $params['filepickeroptions'] = $fpoptions;
diff --git a/lib/editor/atto/tests/behat/autosave.feature b/lib/editor/atto/tests/behat/autosave.feature
new file mode 100644 (file)
index 0000000..2f3d17c
--- /dev/null
@@ -0,0 +1,98 @@
+@editor @editor_atto @atto @_file_upload
+Feature: Atto Autosave
+  To reduce frustration, atto should save drafts of my work.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | teacher2 | Teacher | 2 | teacher2@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | teacher2 | C1 | editingteacher |
+    And I log in as "admin"
+    And I navigate to "Atto toolbar settings" node in "Site administration > Plugins > Text editors > Atto HTML editor"
+    And I set the field "Autosave frequency" to "3"
+    And I set the field with xpath "//select[@name='s_editor_atto_autosavefrequency[u]']" to "seconds"
+    And I click on "Save changes" "button"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I set the field with xpath "//select[@name='summary_editor[format]']" to "1"
+    And I click on "Save and display" "button"
+    And I log out
+
+  @javascript
+  Scenario: Restore a draft
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I set the field "Course summary" to "This is my draft"
+    # Wait for the autosave
+    And I wait "5" seconds
+    And I log out
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    # Wait for the autorestore
+    And I wait "2" seconds
+    Then I should see "This is my draft"
+
+  @javascript
+  Scenario: Do not restore a draft if files have been modified
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I set the field "Course summary" to "This is my draft"
+    # Wait for the autosave
+    And I wait "5" seconds
+    And I log out
+    And I log in as "teacher2"
+    And I follow "Manage private files..."
+    And I upload "lib/editor/atto/tests/fixtures/moodle-logo.png" file to "Files" filemanager
+    And I click on "Save changes" "button"
+    And I click on "Dashboard" "link" in the "Navigation" "block"
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I set the field "Course summary" to "<p>Image test</p>"
+    And I select the text in the "Course summary" Atto editor
+    And I click on "Image" "button"
+    And I click on "Browse repositories..." "button"
+    And I click on "Private files" "link"
+    And I click on "moodle-logo.png" "link"
+    And I click on "Select this file" "button"
+    And I set the field "Describe this image" to "It's the Moodle"
+    # Wait for the page to "settle".
+    And I wait until the page is ready
+    And I click on "Save image" "button"
+    And I click on "Save and display" "button"
+    And I log out
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    Then I should not see "This is my draft"
+
+  @javascript
+  Scenario: Do not restore a draft if text has been modified
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I set the field "Course summary" to "This is my draft"
+    # Wait for the autosave
+    And I wait "5" seconds
+    And I log out
+    And I log in as "teacher2"
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I set the field "Course summary" to "Modified text"
+    And I click on "Save and display" "button"
+    And I log out
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    Then I should not see "This is my draft"
+    And I should see "Modified text"
index 08a326e..56343e3 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100;        // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2015051101;        // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2015050500;        // Requires this Moodle version.
 $plugin->component = 'editor_atto';  // Full name of the plugin (used for diagnostics).
index 118d675..ce838c5 100644 (file)
@@ -201,6 +201,28 @@ abstract class texteditor {
      */
     public abstract function supports_repositories();
 
+    /**
+     * @var string $text The text set to the editor in the form.
+     * @since 3.0
+     */
+    protected $text = '';
+
+    /**
+     * Set the text set for this form field. Will be called before "use_editor".
+     * @param string $text The text for the form field.
+     */
+    public function set_text($text) {
+        $this->text = $text;
+    }
+
+    /**
+     * Get the text set for this form field. Can be called from "use_editor".
+     * @return string
+     */
+    public function get_text() {
+        return $this->text;
+    }
+
     /**
      * Add required JS needed for editor
      * @param string $elementid id of text area to be converted to editor
index a463d23..76e534b 100644 (file)
@@ -377,6 +377,7 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element {
         }
 
         // print text area - TODO: add on-the-fly switching, size configuration, etc.
+        $editor->set_text($text);
         $editor->use_editor($id, $this->_options, $fpoptions);
 
         $rows = empty($this->_attributes['rows']) ? 15 : $this->_attributes['rows'];
index 70bbbd1..f4930bd 100644 (file)
@@ -2595,13 +2595,13 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
         // switch next two lines for ol li containers for form items.
         //        $this->_elementTemplates=array('default'=>"\n\t\t".'<li class="fitem"><label>{label}{help}<!-- BEGIN required -->{req}<!-- END required --></label><div class="qfelement<!-- BEGIN error --> error<!-- END error --> {type}"><!-- BEGIN error --><span class="error">{error}</span><br /><!-- END error -->{element}</div></li>');
         $this->_elementTemplates = array(
-        'default'=>"\n\t\t".'<div id="{id}" class="fitem {advanced}<!-- BEGIN required --> required<!-- END required --> fitem_{type} {emptylabel}" {aria-live}><div class="fitemtitle"><label>{label}<!-- BEGIN required -->{req}<!-- END required -->{advancedimg} </label>{help}</div><div class="felement {type}<!-- BEGIN error --> error<!-- END error -->"><!-- BEGIN error --><span class="error">{error}</span><br /><!-- END error -->{element}</div></div>',
+        'default'=>"\n\t\t".'<div id="{id}" class="fitem {advanced}<!-- BEGIN required --> required<!-- END required --> fitem_{type} {emptylabel}" {aria-live}><div class="fitemtitle"><label>{label}<!-- BEGIN required -->{req}<!-- END required -->{advancedimg} </label>{help}</div><div class="felement {type}<!-- BEGIN error --> error<!-- END error -->"><!-- BEGIN error --><span class="error" tabindex="0">{error}</span><br /><!-- END error -->{element}</div></div>',
 
         'actionbuttons'=>"\n\t\t".'<div id="{id}" class="fitem fitem_actionbuttons fitem_{type}"><div class="felement {type}">{element}</div></div>',
 
-        'fieldset'=>"\n\t\t".'<div id="{id}" class="fitem {advanced}<!-- BEGIN required --> required<!-- END required --> fitem_{type} {emptylabel}"><div class="fitemtitle"><div class="fgrouplabel"><label>{label}<!-- BEGIN required -->{req}<!-- END required -->{advancedimg} </label>{help}</div></div><fieldset class="felement {type}<!-- BEGIN error --> error<!-- END error -->"><!-- BEGIN error --><span class="error">{error}</span><br /><!-- END error -->{element}</fieldset></div>',
+        'fieldset'=>"\n\t\t".'<div id="{id}" class="fitem {advanced}<!-- BEGIN required --> required<!-- END required --> fitem_{type} {emptylabel}"><div class="fitemtitle"><div class="fgrouplabel"><label>{label}<!-- BEGIN required -->{req}<!-- END required -->{advancedimg} </label>{help}</div></div><fieldset class="felement {type}<!-- BEGIN error --> error<!-- END error -->"><!-- BEGIN error --><span class="error" tabindex="0">{error}</span><br /><!-- END error -->{element}</fieldset></div>',
 
-        'static'=>"\n\t\t".'<div class="fitem {advanced} {emptylabel}"><div class="fitemtitle"><div class="fstaticlabel">{label}<!-- BEGIN required -->{req}<!-- END required -->{advancedimg} {help}</div></div><div class="felement fstatic <!-- BEGIN error --> error<!-- END error -->"><!-- BEGIN error --><span class="error">{error}</span><br /><!-- END error -->{element}</div></div>',
+        'static'=>"\n\t\t".'<div class="fitem {advanced} {emptylabel}"><div class="fitemtitle"><div class="fstaticlabel">{label}<!-- BEGIN required -->{req}<!-- END required -->{advancedimg} {help}</div></div><div class="felement fstatic <!-- BEGIN error --> error<!-- END error -->"><!-- BEGIN error --><span class="error" tabindex="0">{error}</span><br /><!-- END error -->{element}</div></div>',
 
         'warning'=>"\n\t\t".'<div class="fitem {advanced} {emptylabel}">{element}</div>',
 
index e013d6d..a341fbf 100644 (file)
@@ -373,7 +373,7 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
                     $newfield = 'profile_field_'.$field->datatype;
                     $formfield = new $newfield($field->id, $user->id);
                     if ($formfield->is_visible() and !$formfield->is_empty()) {
-                        $node = new core_user\output\myprofile\node('contact', $formfield->field->shortname,
+                        $node = new core_user\output\myprofile\node('contact', 'custom_field_' . $formfield->field->shortname,
                             format_string($formfield->field->name), null, null, $formfield->display_data());
                         $tree->add_node($node);
                     }
index 33c2ad5..f3da8f6 100644 (file)
@@ -4509,18 +4509,23 @@ class settings_navigation extends navigation_node {
         $blocknode->force_open();
 
         // Assign local roles
-        $assignurl = new moodle_url('/'.$CFG->admin.'/roles/assign.php', array('contextid'=>$this->context->id));
-        $blocknode->add(get_string('assignroles', 'role'), $assignurl, self::TYPE_SETTING);
+        if (get_assignable_roles($this->context, ROLENAME_ORIGINAL)) {
+            $assignurl = new moodle_url('/'.$CFG->admin.'/roles/assign.php', array('contextid' => $this->context->id));
+            $blocknode->add(get_string('assignroles', 'role'), $assignurl, self::TYPE_SETTING, null,
+                'roles', new pix_icon('i/assignroles', ''));
+        }
 
         // Override roles
         if (has_capability('moodle/role:review', $this->context) or  count(get_overridable_roles($this->context))>0) {
             $url = new moodle_url('/'.$CFG->admin.'/roles/permissions.php', array('contextid'=>$this->context->id));
-            $blocknode->add(get_string('permissions', 'role'), $url, self::TYPE_SETTING);
+            $blocknode->add(get_string('permissions', 'role'), $url, self::TYPE_SETTING, null,
+                'permissions', new pix_icon('i/permissions', ''));
         }
         // Check role permissions
         if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $this->context)) {
             $url = new moodle_url('/'.$CFG->admin.'/roles/check.php', array('contextid'=>$this->context->id));
-            $blocknode->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING);
+            $blocknode->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING, null,
+                'checkpermissions', new pix_icon('i/checkpermissions', ''));
         }
 
         return $blocknode;
index 35e2f34..87e5c73 100644 (file)
@@ -1660,11 +1660,11 @@ class theme_config {
         global $CFG;
         if ($this->usesvg === null) {
 
-            if (!isset($CFG->svgicons) || !is_bool($CFG->svgicons)) {
+            if (!isset($CFG->svgicons)) {
                 $this->usesvg = core_useragent::supports_svg();
             } else {
                 // Force them on/off depending upon the setting.
-                $this->usesvg = $CFG->svgicons;
+                $this->usesvg = (bool)$CFG->svgicons;
             }
         }
         return $this->usesvg;
index d08fb76..d87ce6a 100644 (file)
@@ -3769,7 +3769,10 @@ EOD;
                 $additionalclasses[] = 'docked-region-'.$region;
             }
         }
-        if (count($usedregions) === 1) {
+        if (!$usedregions) {
+            // No regions means there is only content, add 'content-only' class.
+            $additionalclasses[] = 'content-only';
+        } else if (count($usedregions) === 1) {
             // Add the -only class for the only used region.
             $region = array_shift($usedregions);
             $additionalclasses[] = $region . '-only';
index 114a55b..8114893 100644 (file)
@@ -534,7 +534,7 @@ class phpunit_util extends testing_util {
 
         $template = '
         <testsuites>
-            <testsuite name="@component@">
+            <testsuite name="@component@_testsuite">
                 <directory suffix="_test.php">.</directory>
             </testsuite>
         </testsuites>';
index 8312c92..74696f8 100644 (file)
@@ -81,6 +81,9 @@ function rss_get_link($contextid, $userid, $componentname, $id, $tooltiptext='')
 function rss_get_url($contextid, $userid, $componentname, $additionalargs) {
     global $CFG;
     require_once($CFG->libdir.'/filelib.php');
+    if (empty($userid)) {
+        $userid = guest_user()->id;
+    }
     $usertoken = rss_get_token($userid);
     return get_file_url($contextid.'/'.$usertoken.'/'.$componentname.'/'.$additionalargs.'/rss.xml', null, 'rssfile');
 }
index cf5df35..4b165b5 100644 (file)
@@ -1692,45 +1692,6 @@ function make_localcache_directory($directory, $exceptiononerror = true) {
     return make_writable_directory("$CFG->localcachedir/$directory", $exceptiononerror);
 }
 
-/**
- * Checks if current user is a web crawler.
- *
- * This list can not be made complete, this is not a security
- * restriction, we make the list only to help these sites
- * especially when automatic guest login is disabled.
- *
- * If admin needs security they should enable forcelogin
- * and disable guest access!!
- *
- * @return bool
- */
-function is_web_crawler() {
-    if (!empty($_SERVER['HTTP_USER_AGENT'])) {
-        if (strpos($_SERVER['HTTP_USER_AGENT'], 'Googlebot') !== false ) {
-            return true;
-        } else if (strpos($_SERVER['HTTP_USER_AGENT'], 'google.com') !== false ) { // Google
-            return true;
-        } else if (strpos($_SERVER['HTTP_USER_AGENT'], 'Yahoo! Slurp') !== false ) {  // Yahoo
-            return true;
-        } else if (strpos($_SERVER['HTTP_USER_AGENT'], '[ZSEBOT]') !== false ) {  // Zoomspider
-            return true;
-        } else if (stripos($_SERVER['HTTP_USER_AGENT'], 'msnbot') !== false ) {  // MSN Search
-            return true;
-        } else if (strpos($_SERVER['HTTP_USER_AGENT'], 'bingbot') !== false ) {  // Bing
-            return true;
-        } else if (strpos($_SERVER['HTTP_USER_AGENT'], 'Yandex') !== false ) {
-            return true;
-        } else if (strpos($_SERVER['HTTP_USER_AGENT'], 'AltaVista') !== false ) {
-            return true;
-        } else if (stripos($_SERVER['HTTP_USER_AGENT'], 'baiduspider') !== false ) {  // Baidu
-            return true;
-        } else if (strpos($_SERVER['HTTP_USER_AGENT'], 'Teoma') !== false ) {  // Ask.com
-            return true;
-        }
-    }
-    return false;
-}
-
 /**
  * This class solves the problem of how to initialise $OUTPUT.
  *
diff --git a/lib/templates/permissionmanager_panelcontent.mustache b/lib/templates/permissionmanager_panelcontent.mustache
new file mode 100644 (file)
index 0000000..36a1ac5
--- /dev/null
@@ -0,0 +1,44 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core/permissionmanager_panelcontent
+
+    Template which defines the inner content of panel when selecting roles.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * confirmation Confirmation text
+    * roles array of role details
+
+    Example context (json):
+    { "message": "Do you really want to remove Non-editing teacher from the list of allowed roles for capability
+                View added and updated modules in recent activity block?",
+      "roles": [{"roleid": 1, "rolename": "manager", "disabled":"disabled"}]}
+}}
+<div class="popup_content" style="text-align:center;">
+    {{message}} <hr/>
+    <div class="role_buttons">
+        {{#roles}}
+            <input type="button" value="{{rolename}}" data-role-id="{{roleid}}" {{disabled}}/>
+        {{/roles}}
+    </div>
+</div>
diff --git a/lib/templates/permissionmanager_role.mustache b/lib/templates/permissionmanager_role.mustache
new file mode 100644 (file)
index 0000000..3f16ed5
--- /dev/null
@@ -0,0 +1,53 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core/permissionmanager_role
+
+    Template which defines the role name in the capability table.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * role-id
+    * action
+
+    Context variables required for this template:
+    * rolename Name of the role rendered
+    * roleid Id of the role
+    * action WEhich action is done on click
+    * spanclass class attribute of span
+    * linkclass class attribute of link
+    * adminurl moodle admin url
+    * imageurl moodle url for delete(x) image
+
+    Example context (json):
+    {"rolename" : "Manager",
+     "roleid : 1,
+     "action": "prevent",
+     "spanclass": "allowed",
+     "linkclass": "preventlink",
+     "adminurl" : "http://localhost/moodle/admin/",
+     "imageurl": "http://localhost/moodle/theme/image.php?theme=base&component=core&image=t%2Fdelete"}
+}}
+<span style="display:inline-block;" class="{{spanclass}}">&nbsp;{{rolename}}&nbsp;
+    {{#imageurl}}
+        <a href="{{adminurl}}roles/permissions.php" class="{{linkclass}}" data-role-id="{{roleid}}" data-action="{{action}}">
+            <img src="{{imageurl}}" alt="{{action}}" />
+        </a>
+    {{/imageurl}}
+</span>
\ No newline at end of file
index bda221b..b46cd64 100644 (file)
@@ -73,53 +73,6 @@ class core_setuplib_testcase extends advanced_testcase {
                 get_docs_url('%%WWWROOT%%/lib/tests/setuplib_test.php'));
     }
 
-    public function test_is_web_crawler() {
-        $browsers = array(
-            'Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))',
-            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:18.0) Gecko/18.0 Firefox/18.0',
-            'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412 (KHTML, like Gecko) Safari/412',
-            'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10',
-            'Opera/9.0 (Windows NT 5.1; U; en)',
-            'Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17 â€“Nexus',
-            'Mozilla/5.0 (iPad; U; CPU OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5',
-        );
-        $crawlers = array(
-            // Google.
-            'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
-            'Googlebot/2.1 (+http://www.googlebot.com/bot.html)',
-            'Googlebot-Image/1.0',
-            // Yahoo.
-            'Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)',
-            // Bing.
-            'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',
-            'Mozilla/5.0 (compatible; bingbot/2.0 +http://www.bing.com/bingbot.htm)',
-            // MSN.
-            'msnbot/2.1',
-            // Yandex.
-            'Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)',
-            'Mozilla/5.0 (compatible; YandexImages/3.0; +http://yandex.com/bots)',
-            // AltaVista.
-            'AltaVista V2.0B crawler@evreka.com',
-            // ZoomSpider.
-            'ZoomSpider - wrensoft.com [ZSEBOT]',
-            // Baidu.
-            'Baiduspider+(+http://www.baidu.com/search/spider_jp.html)',
-            'Baiduspider+(+http://www.baidu.com/search/spider.htm)',
-            'BaiDuSpider',
-            // Ask.com.
-            'User-Agent: Mozilla/2.0 (compatible; Ask Jeeves/Teoma)',
-        );
-
-        foreach ($browsers as $agent) {
-            $_SERVER['HTTP_USER_AGENT'] = $agent;
-            $this->assertFalse(is_web_crawler());
-        }
-        foreach ($crawlers as $agent) {
-            $_SERVER['HTTP_USER_AGENT'] = $agent;
-            $this->assertTrue(is_web_crawler(), "$agent should be considered a search engine");
-        }
-    }
-
     /**
      * Test if get_exception_info() removes file system paths.
      */
index b4b230c..cb7ac3d 100644 (file)
@@ -1091,6 +1091,235 @@ class core_useragent_testcase extends basic_testcase {
                     'supports_svg'                  => false,
                ),
             ),
+
+            // Google web crawlers.
+            array(
+                'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+            array(
+                'Googlebot/2.1 (+http://www.googlebot.com/bot.html)',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+            array(
+                'Googlebot-Image/1.0',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+
+            // Yahoo crawlers.
+            // See https://help.yahoo.com/kb/slurp-crawling-page-sln22600.html.
+            array(
+                'Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+
+            // Bing / MSN / AdIdx crawlers.
+            // See http://www.bing.com/webmaster/help/which-crawlers-does-bing-use-8c184ec0.
+            array(
+                'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+            array(
+                'Mozilla/5.0 (compatible; bingbot/2.0 +http://www.bing.com/bingbot.htm)',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+            array(
+                'Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',
+                array(
+                    'is_web_crawler'                => true,
+                    'is_webkit'                     => true,
+                    'is_safari_ios'                 => true,
+                    'check_safari_ios_version'      => array(
+                        '527'                       => true,
+                    ),
+
+                    'versionclasses'                => array(
+                        'safari',
+                        'ios',
+                    ),
+
+                    'devicetype'                    => 'mobile',
+               ),
+            ),
+            array(
+                'Mozilla/5.0 (Windows Phone 8.1; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 530) like Gecko (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',
+                array(
+                    'is_web_crawler'                => true,
+                    'is_ie'                         => true,
+                    'check_ie_version'              => array(
+                        '0'                         => true,
+                        '5.0'                       => true,
+                        '5.5'                       => true,
+                        '6.0'                       => true,
+                        '7.0'                       => true,
+                        '8.0'                       => true,
+                        '9.0'                       => true,
+                        '10'                        => true,
+                        '11'                        => true,
+                    ),
+                    'versionclasses'                => array(
+                        'ie',
+                        'ie11',
+                    ),
+                    'devicetype'                    => 'mobile',
+               ),
+            ),
+
+            array(
+                'msnbot/2.0b (+http://search.msn.com/msnbot.htm)',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+            array(
+                'msnbot/2.1',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+            array(
+                'msnbot-media/1.1 (+http://search.msn.com/msnbot.htm)',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+            array(
+                'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534+ (KHTML, like Gecko) BingPreview/1.0b',
+                array(
+                    'is_web_crawler'                => true,
+                    'is_webkit'                     => true,
+                    'is_safari'                     => true,
+                    'check_safari_version'          => array(
+                        '1'                         => true,
+                        '312'                       => true,
+                        '500'                       => true,
+                    ),
+
+                    'versionclasses'                => array(
+                        'safari',
+                    ),
+               ),
+            ),
+            array(
+                'Mozilla/5.0 (Windows Phone 8.1; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 530) like Gecko BingPreview/1.0b',
+                array(
+                    'is_web_crawler'                => true,
+                    'is_ie'                         => true,
+                    'check_ie_version'              => array(
+                        '0'                         => true,
+                        '5.0'                       => true,
+                        '5.5'                       => true,
+                        '6.0'                       => true,
+                        '7.0'                       => true,
+                        '8.0'                       => true,
+                        '9.0'                       => true,
+                        '10'                        => true,
+                        '11'                        => true,
+                    ),
+                    'versionclasses'                => array(
+                        'ie',
+                        'ie11',
+                    ),
+                    'devicetype'                    => 'mobile',
+               ),
+            ),
+
+            // Yandex.
+            // See http://help.yandex.com/search/robots/agent.xml.
+            array(
+                'Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+            array(
+                'Mozilla/5.0 (compatible; YandexImages/3.0; +http://yandex.com/bots)',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+
+            // AltaVista.
+            array(
+                'AltaVista V2.0B crawler@evreka.com',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+
+            // ZoomSpider.
+            array(
+                'ZoomSpider - wrensoft.com [ZSEBOT]',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+
+            // Baidu.
+            array(
+                'Baiduspider+(+http://www.baidu.com/search/spider_jp.html)',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+            array(
+                'Baiduspider+(+http://www.baidu.com/search/spider.htm)',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
+
+            // Ask.com.
+            array(
+                'User-Agent: Mozilla/2.0 (compatible; Ask Jeeves/Teoma)',
+                array(
+                    'is_web_crawler'                => true,
+                    'versionclasses'                => array(
+                    ),
+               ),
+            ),
         );
     }
 
@@ -1525,4 +1754,15 @@ class core_useragent_testcase extends basic_testcase {
         }
         $this->assertCount(count($tests['versionclasses']), $actual);
     }
+
+    /**
+     * @dataProvider user_agents_providers
+     */
+    public function test_useragent_web_crawler($useragent, $tests) {
+        // Setup the core_useragent instance.
+        core_useragent::instance(true, $useragent);
+
+        $expectation = isset($tests['is_web_crawler']) ? $tests['is_web_crawler'] : false;
+        $this->assertSame($expectation, core_useragent::is_web_crawler());
+    }
 }
index a842ea7..d11e49a 100644 (file)
@@ -3,7 +3,11 @@ information provided here is intended especially for developers.
 
 === 3.0 ===
 
+* All plugins are required to declare their frankenstyle component name via
+  the $plugin->component property in their version.php file. See
+  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.
 * 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
@@ -125,6 +129,9 @@ information provided here is intended especially for developers.
     cohort_get_visible_list()
     enrol_cohort_enrol_all_users()
     enrol_cohort_search_cohorts()
+* The never unused webdav_locks table was dropped.
+* The actionmenu hideMenu() function now expects an EventFacade object to be passed to it,
+  i.e. a call to M.core.actionmenu.instance.hideMenu() should be change to M.core.actionmenu.instance.hideMenu(e)
 
 === 2.9.1 ===
 
index d2d0772..f7dc699 100644 (file)
@@ -450,15 +450,16 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) {
         require($fullplug.'/version.php');  // defines $plugin with version etc
         unset($module);
 
-        // if plugin tells us it's full name we may check the location
-        if (isset($plugin->component)) {
-            if ($plugin->component !== $component) {
-                throw new plugin_misplaced_exception($plugin->component, null, $fullplug);
-            }
+        if (empty($plugin->version)) {
+            throw new plugin_defective_exception($component, 'Missing $plugin->version number in version.php.');
         }
 
-        if (empty($plugin->version)) {
-            throw new plugin_defective_exception($component, 'Missing version value in version.php');
+        if (empty($plugin->component)) {
+            throw new plugin_defective_exception($component, 'Missing $plugin->component declaration in version.php.');
+        }
+
+        if ($plugin->component !== $component) {
+            throw new plugin_misplaced_exception($plugin->component, null, $fullplug);
         }
 
         $plugin->name     = $plug;
@@ -600,27 +601,33 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) {
             throw new plugin_defective_exception($component, 'Missing version.php');
         }
 
-        // TODO: Support for $module will end with Moodle 2.10 by MDL-43896. Was deprecated for Moodle 2.7 by MDL-43040.
+        $module = new stdClass();
         $plugin = new stdClass();
         $plugin->version = null;
-        $module = $plugin;
         require($fullmod .'/version.php');  // Defines $plugin with version etc.
-        $plugin = clone($module);
+
+        // Check if the legacy $module syntax is still used.
+        if (!is_object($module) or (count((array)$module) > 0)) {
+            throw new plugin_defective_exception($component, 'Unsupported $module syntax detected in version.php');
+        }
+
+        // Prepare the record for the {modules} table.
+        $module = clone($plugin);
         unset($module->version);
         unset($module->component);
         unset($module->dependencies);
         unset($module->release);
 
-        // if plugin tells us it's full name we may check the location
-        if (isset($plugin->component)) {
-            if ($plugin->component !== $component) {
-                throw new plugin_misplaced_exception($plugin->component, null, $fullmod);
-            }
+        if (empty($plugin->version)) {
+            throw new plugin_defective_exception($component, 'Missing $plugin->version number in version.php.');
         }
 
-        if (empty($plugin->version)) {
-            // Version must be always set now!
-            throw new plugin_defective_exception($component, 'Missing version value in version.php');
+        if (empty($plugin->component)) {
+            throw new plugin_defective_exception($component, 'Missing $plugin->component declaration in version.php.');
+        }
+
+        if ($plugin->component !== $component) {
+            throw new plugin_misplaced_exception($plugin->component, null, $fullmod);
         }
 
         if (!empty($plugin->requires)) {
@@ -792,15 +799,16 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) {
         unset($block->dependencies);
         unset($block->release);
 
-        // if plugin tells us it's full name we may check the location
-        if (isset($plugin->component)) {
-            if ($plugin->component !== $component) {
-                throw new plugin_misplaced_exception($plugin->component, null, $fullblock);
-            }
+        if (empty($plugin->version)) {
+            throw new plugin_defective_exception($component, 'Missing block version number in version.php.');
         }
 
-        if (empty($plugin->version)) {
-            throw new plugin_defective_exception($component, 'Missing block version.');
+        if (empty($plugin->component)) {
+            throw new plugin_defective_exception($component, 'Missing $plugin->component declaration in version.php.');
+        }
+
+        if ($plugin->component !== $component) {
+            throw new plugin_misplaced_exception($plugin->component, null, $fullblock);
         }
 
         if (!empty($plugin->requires)) {
index d666012..3102cd2 100644 (file)
Binary files a/lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-debug.js and b/lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-debug.js differ
index cf33cb3..3f0038c 100644 (file)
Binary files a/lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-min.js and b/lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-min.js differ
index 922a730..df75a4c 100644 (file)
Binary files a/lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu.js and b/lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu.js differ
index 758c61f..bd12367 100644 (file)
Binary files a/lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js and b/lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js differ
index 99afb54..293b9c1 100644 (file)
Binary files a/lib/yui/build/moodle-core-dock/moodle-core-dock-min.js and b/lib/yui/build/moodle-core-dock/moodle-core-dock-min.js differ
index 0a52f18..7e6f8cd 100644 (file)
Binary files a/lib/yui/build/moodle-core-dock/moodle-core-dock.js and b/lib/yui/build/moodle-core-dock/moodle-core-dock.js differ
index 44de60f..f291a9a 100644 (file)
@@ -245,9 +245,10 @@ ACTIONMENU.prototype = {
 
     /**
      * Hides the menu if it is visible.
+     * @param {EventFacade} e
      * @method hideMenu
      */
-    hideMenu : function() {
+    hideMenu : function(e) {
         if (this.dialogue) {
             Y.log('Hiding an action menu', 'debug', ACTIONMENU.NAME);
             this.dialogue.removeClass('show');
@@ -266,7 +267,9 @@ ACTIONMENU.prototype = {
         }
 
         if (this.menulink) {
-            this.menulink.focus();
+            if (e.type != 'click') {
+                this.menulink.focus();
+            }
             this.menulink = null;
         }
     },
@@ -293,7 +296,7 @@ ACTIONMENU.prototype = {
 
         // Prevent event propagation as it will trigger the hideIfOutside event handler in certain situations.
         e.halt(true);
-        this.hideMenu();
+        this.hideMenu(e);
         if (menuvisible) {
             // The menu was visible and the user has clicked to toggle it again.
             return;
@@ -322,7 +325,7 @@ ACTIONMENU.prototype = {
                 this.lastMenuChild.focus();
                 e.preventDefault();
             } else if (e.keyCode === 9 && e.shiftKey) {
-                this.hideMenu();
+                this.hideMenu(e);
                 e.preventDefault();
             }
             return this;
@@ -330,7 +333,7 @@ ACTIONMENU.prototype = {
 
         if (e.keyCode === 27) {
             // The escape key was pressed so close the menu.
-            this.hideMenu();
+            this.hideMenu(e);
             e.preventDefault();
 
         } else if (e.keyCode === 32) {
@@ -342,10 +345,10 @@ ACTIONMENU.prototype = {
             // We only override the Shift + Tab on the first option, and Tab on the last option to change where the
             // focus is moved to.
             if (e.target === this.firstMenuChild && e.shiftKey) {
-                this.hideMenu();
+                this.hideMenu(e);
                 e.preventDefault();
             } else if (e.target === this.lastMenuChild && !e.shiftKey) {
-                if (this.hideMenu()) {
+                if (this.hideMenu(e)) {
                     // Determine the next selector and focus on it.
                     next = this.menulink.next(SELECTOR.CAN_RECEIVE_FOCUS_SELECTOR);
                     if (next) {
@@ -412,7 +415,7 @@ ACTIONMENU.prototype = {
      */
     hideIfOutside : function(e) {
         if (!e.target.ancestor(SELECTOR.MENUCHILD, true)) {
-            this.hideMenu();
+            this.hideMenu(e);
         }
     },
 
@@ -467,7 +470,7 @@ ACTIONMENU.prototype = {
             if (e.currentTarget.test(SELECTOR.KEEPOPEN)) {
                 return;
             }
-            this.hideMenu();
+            this.hideMenu(e);
         }, SELECTOR.MENUCHILD, this));
 
         return true;
index cb078a4..824fd3c 100644 (file)
@@ -58,8 +58,7 @@ BLOCK.prototype = {
      * @method initializer
      */
     initializer : function() {
-        var node = Y.one('#inst'+this.get('id')),
-            commands;
+        var node = Y.one('#inst'+this.get('id'));
         if (!node) {
             return false;
         }
@@ -71,14 +70,7 @@ BLOCK.prototype = {
         // Move the block straight to the dock if required
         if (node.hasClass(CSS.dockonload)) {
             node.removeClass(CSS.dockonload);
-            commands = node.one('.header .title .commands');
-            if (!commands) {
-                commands = Y.Node.create('<div class="commands"></div>');
-                if (node.one('.header .title')) {
-                    node.one('.header .title').append(commands);
-                }
-            }
-            this.moveToDock(null, commands);
+            this.moveToDock();
         }
         this.skipsetposition = false;
         return true;
@@ -134,8 +126,15 @@ BLOCK.prototype = {
         this.recordBlockState();
 
         blocktitle = this.cachedcontentnode.one('.title h2').cloneNode(true);
-        blockcommands = this.cachedcontentnode.one('.title .commands').cloneNode(true);
 
+        // Build up the block commands.
+        // These should not actually added to the DOM.
+        blockcommands = this.cachedcontentnode.one('.title .commands');
+        if (blockcommands) {
+            blockcommands = blockcommands.cloneNode(true);
+        } else {
+            blockcommands = Y.Node.create('<div class="commands"></div>');
+        }
         movetoimg = Y.Node.create('<img />').setAttrs({
             alt : Y.Escape.html(M.util.get_string('undockitem', 'block')),
             title : Y.Escape.html(M.util.get_string('undockblock', 'block', blocktitle.get('innerHTML'))),
@@ -186,9 +185,6 @@ BLOCK.prototype = {
         node.replace(this.contentplaceholder);
         dock.addToHoldingArea(node);
         node = null;
-        if (!this.cachedcontentnode.one('.title .commands')) {
-            this.cachedcontentnode.one('.title').append(Y.Node.create('<div class="commands"></div>'));
-        }
     },
 
     /**
@@ -197,8 +193,7 @@ BLOCK.prototype = {
      * @return {Boolean}
      */
     returnToPage : function() {
-        var id = this.get('id'),
-            commands;
+        var id = this.get('id');
 
         Y.log('Moving block out of the dock:'+this.get('id'), 'debug', LOGNS);
 
@@ -214,15 +209,8 @@ BLOCK.prototype = {
         }
 
         this.contentplaceholder.replace(this.cachedcontentnode);
-        this.cachedcontentnode = Y.one('#'+this.cachedcontentnode.get('id'));
-
-        commands = this.dockitem.get('commands');
-        if (commands) {
-            commands.all('.hidepanelicon').remove();
-            commands.all('.moveto').remove();
-            commands.remove();
-        }
         this.cachedcontentnode = null;
+
         M.util.set_user_preference('docked_block_instance_'+id, 0);
         this.set('isDocked', false);
         return true;
index 61f99ea..8e03abc 100644 (file)
@@ -426,7 +426,6 @@ DOCK.prototype = {
         Y.delegate('mouseenter', this.handleEvent, this.get('dockNode'), '.'+CSS.dockedtitle, this, mouseenterargs);
         this.get('dockNode').on('mouseleave', this.handleEvent, this, {cssselector:'#dock', delay:0.5, iscontained:false});
 
-        Y.delegate('click', this.handleReturnToBlock, this.get('dockNode'), SELECTOR.panelmoveto, this);
         Y.delegate('click', this.handleReturnToBlock, this.get('dockNode'), SELECTOR.panelmoveto, this);
         Y.delegate('dock:actionkey', this.handleDockedItemEvent, this.get('dockNode'), '.'+CSS.dockeditem, this);
 
index 66e622c..8e26ee6 100644 (file)
@@ -101,5 +101,6 @@ $PAGE->set_title($newaccount);
 $PAGE->set_heading($SITE->fullname);
 
 echo $OUTPUT->header();
+echo $OUTPUT->heading($newaccount);
 $mform_signup->display();
 echo $OUTPUT->footer();
index 83ce9fc..281c76d 100644 (file)
@@ -41,24 +41,24 @@ class login_signup_form extends moodleform {
 
         $mform->addElement('text', 'username', get_string('username'), 'maxlength="100" size="12"');
         $mform->setType('username', PARAM_NOTAGS);
-        $mform->addRule('username', get_string('missingusername'), 'required', null, 'server');
+        $mform->addRule('username', get_string('missingusername'), 'required', null, 'client');
 
         if (!empty($CFG->passwordpolicy)){
             $mform->addElement('static', 'passwordpolicyinfo', '', print_password_policy());
         }
         $mform->addElement('passwordunmask', 'password', get_string('password'), 'maxlength="32" size="12"');
         $mform->setType('password', PARAM_RAW);
-        $mform->addRule('password', get_string('missingpassword'), 'required', null, 'server');
+        $mform->addRule('password', get_string('missingpassword'), 'required', null, 'client');
 
         $mform->addElement('header', 'supplyinfo', get_string('supplyinfo'),'');
 
         $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="25"');
         $mform->setType('email', PARAM_RAW_TRIMMED);
-        $mform->addRule('email', get_string('missingemail'), 'required', null, 'server');
+        $mform->addRule('email', get_string('missingemail'), 'required', null, 'client');
 
         $mform->addElement('text', 'email2', get_string('emailagain'), 'maxlength="100" size="25"');
         $mform->setType('email2', PARAM_RAW_TRIMMED);
-        $mform->addRule('email2', get_string('missingemail'), 'required', null, 'server');
+        $mform->addRule('email2', get_string('missingemail'), 'required', null, 'client');
 
         $namefields = useredit_get_required_name_fields();
         foreach ($namefields as $field) {
@@ -68,7 +68,7 @@ class login_signup_form extends moodleform {
             if (!get_string_manager()->string_exists($stringid, 'moodle')) {
                 $stringid = 'required';
             }
-            $mform->addRule($field, get_string($stringid), 'required', null, 'server');
+            $mform->addRule($field, get_string($stringid), 'required', null, 'client');
         }
 
         $mform->addElement('text', 'city', get_string('city'), 'maxlength="120" size="20"');
@@ -101,7 +101,7 @@ class login_signup_form extends moodleform {
             $mform->setExpanded('policyagreement');
             $mform->addElement('static', 'policylink', '', '<a href="'.$CFG->sitepolicy.'" onclick="this.target=\'_blank\'">'.get_String('policyagreementclick').'</a>');
             $mform->addElement('checkbox', 'policyagreed', get_string('policyaccept'));
-            $mform->addRule('policyagreed', get_string('policyagree'), 'required', null, 'server');
+            $mform->addRule('policyagreed', get_string('policyagree'), 'required', null, 'client');
         }
 
         // buttons
index cf9bc1f..eb9bef1 100644 (file)
@@ -85,7 +85,7 @@ $string['navprev'] = 'Previous';
 $string['navnext'] = 'Next';
 $string['navexit'] = 'Exit book';
 $string['book:addinstance'] = 'Add a new book';
-$string['book:read'] = 'Read book';
+$string['book:read'] = 'View book';
 $string['book:edit'] = 'Edit book chapters';
 $string['book:viewhiddenchapters'] = 'View hidden book chapters';
 $string['errorchapter'] = 'Error reading chapter of book.';
index bf9a2d3..492caa8 100644 (file)
@@ -58,7 +58,7 @@ $string['chat:exportsession'] = 'Export any chat session';
 $string['chat:chat'] = 'Access a chat room';
 $string['chatintro'] = 'Description';
 $string['chatname'] = 'Name of this chat room';
-$string['chat:readlog'] = 'Read chat logs';
+$string['chat:readlog'] = 'View chat logs';
 $string['chatreport'] = 'Chat sessions';
 $string['chat:talk'] = 'Talk in a chat';
 $string['chattime'] = 'Next chat time';
index 8e57741..48553ed 100644 (file)
@@ -57,7 +57,7 @@ You can fill in any number of these. If you leave some of the options blank, the
 $string['limitanswers_help'] = 'This option allows you to limit the number of participants that can select each choice option. When the limit is reached then no-one else can select that option.
 
 If limits are disabled then any number of participants can select each of the options.';
-$string['choice:readresponses'] = 'Read responses';
+$string['choice:readresponses'] = 'View responses';
 $string['choicesaved'] = 'Your choice has been saved';
 $string['choicetext'] = 'Choice text';
 $string['chooseaction'] = 'Choose an action ...';
index 36973a4..a6e1b4c 100644 (file)
@@ -144,6 +144,7 @@ class data_field_textarea extends data_field_base {
         foreach ($formats as $fid) {
             $formats[$fid] = $strformats[$fid];
         }
+        $editor->set_text($text);
         $editor->use_editor($field, $options, $fpoptions);
         $str .= '<input type="hidden" name="'.$field.'_itemid" value="'.$draftitemid.'" />';
         $str .= '<div class="mod-data-input">';
index bcf374d..06d1a7b 100644 (file)
@@ -218,6 +218,7 @@ if ($mode == 'listtemplate'){
     echo '<div class="template_heading"><label for="edit-listtemplateheader">'.get_string('header','data').'</label></div>';
 
     $field = 'listtemplateheader';
+    $editor->set_text($data->listtemplateheader);
     $editor->use_editor($field, $options);
     echo '<div><textarea id="'.$field.'" name="'.$field.'" rows="15" cols="80">'.s($data->listtemplateheader).'</textarea></div>';
 
@@ -316,6 +317,7 @@ if ($mode == 'listtemplate'){
 }
 
 $field = 'template';
+$editor->set_text($data->{$mode});
 $editor->use_editor($field, $options);
 echo '<div><textarea id="'.$field.'" name="'.$field.'" rows="15" cols="80">'.s($data->{$mode}).'</textarea></div>';
 echo '</td>';
@@ -328,6 +330,7 @@ if ($mode == 'listtemplate'){
     echo '<div class="template_heading"><label for="edit-listtemplatefooter">'.get_string('footer','data').'</label></div>';
 
     $field = 'listtemplatefooter';
+    $editor->set_text($data->listtemplatefooter);
     $editor->use_editor($field, $options);
     echo '<div><textarea id="'.$field.'" name="'.$field.'" rows="15" cols="80">'.s($data->listtemplatefooter).'</textarea></div>';
     echo '</td>';
@@ -339,6 +342,7 @@ if ($mode == 'listtemplate'){
     echo '<div class="template_heading"><label for="edit-rsstitletemplate">'.get_string('rsstitletemplate','data').'</label></div>';
 
     $field = 'rsstitletemplate';
+    $editor->set_text($data->rsstitletemplate);
     $editor->use_editor($field, $options);
     echo '<div><textarea id="'.$field.'" name="'.$field.'" rows="15" cols="80">'.s($data->rsstitletemplate).'</textarea></div>';
     echo '</td>';
index 523b79c..6d53e6a 100644 (file)
@@ -159,31 +159,67 @@ class mod_forum_post_form extends moodleform {
             $mform->setConstants(array('timestart'=> 0, 'timeend'=>0));
         }
 
-        if ($groupmode = groups_get_activity_groupmode($cm, $course)) { // hack alert
+        if ($groupmode = groups_get_activity_groupmode($cm, $course)) {
             $groupdata = groups_get_activity_allowed_groups($cm);
-            $groupcount = count($groupdata);
+
             $groupinfo = array();
-            $modulecontext = context_module::instance($cm->id);
+            foreach ($groupdata as $groupid => $group) {
+                // Check whether this user can post in this group.
+                // We must make this check because all groups are returned for a visible grouped activity.
+                if (forum_user_can_post_discussion($forum, $groupid, null, $cm, $modcontext)) {
+                    // Build the data for the groupinfo select.
+                    $groupinfo[$groupid] = $group->name;
+                } else {
+                    unset($groupdata[$groupid]);
+                }
+            }
+            $groupcount = count($groupinfo);
+
+            // Check whether a user can post to all of their own groups.
+
+            // Posts to all of my groups are copied to each group that the user is a member of. Certain conditions must be met.
+            // 1) It only makes sense to allow this when a user is in more than one group.
+            // Note: This check must come before we consider adding accessallgroups, because that is not a real group.
+            $canposttoowngroups = empty($post->edit) && $groupcount > 1;
+
+            // 2) Important: You can *only* post to multiple groups for a top level post. Never any reply.
+            $canposttoowngroups = $canposttoowngroups && empty($post->parent);
+
+            // 3) You also need the canposttoowngroups capability.
+            $canposttoowngroups = $canposttoowngroups && has_capability('mod/forum:canposttomygroups', $modcontext);
+            if ($canposttoowngroups) {
+                // This user is in multiple groups, and can post to all of their own groups.
+                // Note: This is not the same as accessallgroups. This option will copy a post to all groups that a
+                // user is a member of.
+                $mform->addElement('checkbox', 'posttomygroups', get_string('posttomygroups', 'forum'));
+                $mform->addHelpButton('posttomygroups', 'posttomygroups', 'forum');
+                $mform->disabledIf('groupinfo', 'posttomygroups', 'checked');
+            }
 
-            // Check whether the user has access to all groups in this forum from the accessallgroups cap.
-            if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $modulecontext)) {
-                // Only allow posting to all groups if the user has access to all groups.
-                $groupinfo = array('0' => get_string('allparticipants'));
+            // Check whether this user can post to all groups.
+            // Posts to the 'All participants' group go to all groups, not to each group in a list.
+            // It makes sense to allow this, even if there currently aren't any groups because there may be in the future.
+            if (forum_user_can_post_discussion($forum, -1, null, $cm, $modcontext)) {
+                // Note: We must reverse in this manner because array_unshift renumbers the array.
+                $groupinfo = array_reverse($groupinfo, true );
+                $groupinfo[-1] = get_string('allparticipants');
+                $groupinfo = array_reverse($groupinfo, true );
                 $groupcount++;
             }
 
-            $contextcheck = has_capability('mod/forum:movediscussions', $modulecontext) && empty($post->parent) && $groupcount > 1;
-            if ($contextcheck) {
-                if (has_capability('mod/forum:canposttomygroups', $modulecontext)
-                            && !isset($post->edit)) {
-                    $mform->addElement('checkbox', 'posttomygroups', get_string('posttomygroups', 'forum'));
-                    $mform->addHelpButton('posttomygroups', 'posttomygroups', 'forum');
-                    $mform->disabledIf('groupinfo', 'posttomygroups', 'checked');
-                }
+            // Determine whether the user can select a group from the dropdown. The dropdown is available for several reasons.
+            // 1) This is a new post (not an edit), and there are at least two groups to choose from.
+            $canselectgroupfornew = empty($post->edit) && $groupcount > 1;
 
-                foreach ($groupdata as $grouptemp) {
-                    $groupinfo[$grouptemp->id] = $grouptemp->name;
-                }
+            // 2) This is editing of an existing post and the user is allowed to movediscussions.
+            // We allow this because the post may have been moved from another forum where groups are not available.
+            // We show this even if no groups are available as groups *may* have been available but now are not.
+            $canselectgroupformove = $groupcount && !empty($post->edit) && has_capability('mod/forum:movediscussions', $modcontext);
+
+            // Important: You can *only* change the group for a top level post. Never any reply.
+            $canselectgroup = empty($post->parent) && ($canselectgroupfornew || $canselectgroupformove);
+
+            if ($canselectgroup) {
                 $mform->addElement('select','groupinfo', get_string('group'), $groupinfo);
                 $mform->setDefault('groupinfo', $post->groupid);
                 $mform->setType('groupinfo', PARAM_INT);
index 05fc701..5f4d874 100644 (file)
@@ -3505,6 +3505,15 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
         } else {
             $replystring = get_string('repliesmany', 'forum', $post->replies);
         }
+        if (!empty($discussion->unread) && $discussion->unread !== '-') {
+            $replystring .= ' <span class="sep">/</span> <span class="unread">';
+            if ($discussion->unread == 1) {
+                $replystring .= get_string('unreadpostsone', 'forum');
+            } else {
+                $replystring .= get_string('unreadpostsnumber', 'forum', $discussion->unread);
+            }
+            $replystring .= '</span>';
+        }
 
         $output .= html_writer::start_tag('div', array('class'=>'link'));
         $output .= html_writer::link($discussionlink, get_string('discussthistopic', 'forum'));
index bd6bc18..6e3f5e6 100644 (file)
@@ -697,8 +697,6 @@ if ($mform_post->is_cancelled()) {
     // WARNING: the $fromform->message array has been overwritten, do not use it anymore!
     $fromform->messagetrust  = trusttext_trusted($modcontext);
 
-    $contextcheck = isset($fromform->groupinfo) && has_capability('mod/forum:movediscussions', $modcontext);
-
     if ($fromform->edit) {           // Updating a post
         unset($fromform->groupid);
         $fromform->id = $fromform->edit;
@@ -722,10 +720,15 @@ if ($mform_post->is_cancelled()) {
         }
 
         // If the user has access to all groups and they are changing the group, then update the post.
-        if ($contextcheck) {
+        if (isset($fromform->groupinfo) && has_capability('mod/forum:movediscussions', $modcontext)) {
             if (empty($fromform->groupinfo)) {
                 $fromform->groupinfo = -1;
             }
+
+            if (!forum_user_can_post_discussion($forum, $fromform->groupinfo, null, $cm, $modcontext)) {
+                print_error('cannotupdatepost', 'forum');
+            }
+
             $DB->set_field('forum_discussions' ,'groupid' , $fromform->groupinfo, array('firstpost' => $fromform->id));
         }
 
@@ -853,6 +856,9 @@ if ($mform_post->is_cancelled()) {
         exit;
 
     } else { // Adding a new discussion.
+        // The location to redirect to after successfully posting.
+        $redirectto = new moodle_url('view.php', array('f' => $fromform->forum));
+
         $fromform->mailnow = empty($fromform->mailnow) ? 0 : 1;
 
         $discussion = $fromform;
@@ -870,17 +876,28 @@ if ($mform_post->is_cancelled()) {
 
         // If we are posting a copy to all groups the user has access to.
         if (isset($fromform->posttomygroups)) {
+            // Post to each of my groups.
             require_capability('mod/forum:canposttomygroups', $modcontext);
+
+            // Fetch all of this user's groups.
+            // Note: all groups are returned when in visible groups mode so we must manually filter.
             $allowedgroups = groups_get_activity_allowed_groups($cm);
-            $groupstopostto = array_keys($allowedgroups);
-        } else {
-            if ($contextcheck) {
-                $fromform->groupid = $fromform->groupinfo;
-            }
-            if (empty($fromform->groupid)) {
-                $fromform->groupid = -1;
+            foreach ($allowedgroups as $groupid => $group) {
+                if (forum_user_can_post_discussion($forum, $groupid, -1, $cm, $modcontext)) {
+                    $groupstopostto[] = $groupid;
+                }
             }
-            $groupstopostto = array($fromform->groupid);
+        } else if (isset($fromform->groupinfo)) {
+            // Use the value provided in the dropdown group selection.
+            $groupstopostto[] = $fromform->groupinfo;
+            $redirectto->param('group', $fromform->groupinfo);
+        } else if (isset($fromform->groupid) && !empty($fromform->groupid)) {
+            // Use the value provided in the hidden form element instead.
+            $groupstopostto[] = $fromform->groupid;
+            $redirectto->param('group', $fromform->groupid);
+        } else {
+            // Use the value for all participants instead.
+            $groupstopostto[] = -1;
         }
 
         // Before we post this we must check that the user will not exceed the blocking threshold.
@@ -934,9 +951,8 @@ if ($mform_post->is_cancelled()) {
             $completion->update_state($cm, COMPLETION_COMPLETE);
         }
 
-        redirect(forum_go_back_to("view.php?f=$fromform->forum"), $message.$subscribemessage, $timemessage);
-
-        exit;
+        // Redirect back to the discussion.
+        redirect(forum_go_back_to($redirectto->out()), $message . $subscribemessage, $timemessage);
     }
 }
 
diff --git a/mod/forum/tests/behat/groups_in_course_no_groups_in_forum.feature b/mod/forum/tests/behat/groups_in_course_no_groups_in_forum.feature
new file mode 100644 (file)
index 0000000..ff4e5e3
--- /dev/null
@@ -0,0 +1,62 @@
+@mod @mod_forum
+Feature: Forums in 'No groups' mode allow posting to All participants for all users
+  In order to post to a forum in 'No groups' mode, which is in course which has groups
+  As any user
+  I need to post
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@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 the following "groups" exist:
+      | name | course | idnumber |
+      | Group A | C1 | G1 |
+      | Group B | C1 | G2 |
+    And the following "group members" exist:
+      | user | group |
+      | teacher1 | G1 |
+      | teacher1 | G2 |
+      | student1 | G1 |
+    And the following "activities" exist:
+      | activity   | name                   | intro                         | course | idnumber     | groupmode |
+      | forum      | Standard forum name    | Standard forum description    | C1     | nogroups     | 0         |
+
+  Scenario: Teacher can post
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Standard forum name"
+    And I should not see "Group A"
+    And I should not see "Group B"
+    When I click on "Add a new discussion topic" "button"
+    Then I should not see "Post a copy to all groups"
+    And I should not see "Group" in the "form" "css_element"
+    And I set the following fields to these values:
+      | Subject | Teacher 1 -> Forum  |
+      | Message | Teacher 1 -> Forum  |
+    And I press "Post to forum"
+    And I wait to be redirected
+    And I should see "Teacher 1 -> Forum"
+
+  Scenario: Student can post
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Standard forum name"
+    And I should not see "Group A"
+    And I should not see "Group B"
+    When I click on "Add a new discussion topic" "button"
+    Then I should not see "Post a copy to all groups"
+    And I should not see "Group" in the "form" "css_element"
+    And I set the following fields to these values:
+      | Subject | Student 1 -> Forum  |
+      | Message | Student 1 -> Forum  |
+    And I press "Post to forum"
+    And I wait to be redirected
+    And I should see "Student 1 -> Forum"
diff --git a/mod/forum/tests/behat/no_groups_in_course.feature b/mod/forum/tests/behat/no_groups_in_course.feature
new file mode 100644 (file)
index 0000000..96b672f
--- /dev/null
@@ -0,0 +1,86 @@
+@mod @mod_forum
+Feature: Posting to forums in a course with no groups behaves correctly
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@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 the following "activities" exist:
+      | activity   | name                   | intro                         | course | idnumber     | groupmode |
+      | forum      | Standard forum         | Standard forum description    | C1     | nogroups     | 0         |
+      | forum      | Visible forum          | Visible forum description     | C1     | visgroups    | 2         |
+      | forum      | Separate forum         | Separate forum description    | C1     | sepgroups    | 1         |
+
+  Scenario: Teachers can post in standard forum
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Standard forum"
+    When I click on "Add a new discussion topic" "button"
+    Then I should not see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Teacher -> All participants |
+      | Message | Teacher -> All participants |
+    And I press "Post to forum"
+    And I wait to be redirected
+    And I should see "Teacher -> All participants"
+
+  Scenario: Teachers can post in forum with separate groups
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Separate forum"
+    When I click on "Add a new discussion topic" "button"
+    Then I should not see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Teacher -> All participants |
+      | Message | Teacher -> All participants |
+    And I press "Post to forum"
+    And I wait to be redirected
+    And I should see "Teacher -> All participants"
+
+  Scenario: Teachers can post in forum with visible groups
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Visible forum"
+    When I click on "Add a new discussion topic" "button"
+    Then I should not see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Teacher -> All participants |
+      | Message | Teacher -> All participants |
+    And I press "Post to forum"
+    And I wait to be redirected
+    And I should see "Teacher -> All participants"
+
+  Scenario: Students can post in standard forum
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Standard forum"
+    When I click on "Add a new discussion topic" "button"
+    Then I should not see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Student -> All participants |
+      | Message | Student -> All participants |
+    And I press "Post to forum"
+    And I wait to be redirected
+    And I should see "Student -> All participants"
+
+  Scenario: Students cannot post in forum with separate groups
+    Given I log in as "student1"
+    And I follow "Course 1"
+    When I follow "Separate forum"
+    Then I should see "You do not have permission to add a new discussion topic for all participants."
+    And I should not see "Add a new discussion topic"
+
+  Scenario: Teachers can post in forum with visible groups
+    Given I log in as "student1"
+    And I follow "Course 1"
+    When I follow "Visible forum"
+    Then I should see "You do not have permission to add a new discussion topic for all participants."
+    And I should not see "Add a new discussion topic"
index 86d4e08..2a99b8c 100644 (file)
@@ -2,7 +2,7 @@
 Feature: Posting to all groups in a separate group discussion is restricted to users with access to all groups
   In order to post to all groups in a forum with separate groups
   As a teacher
-  I need to have the accessallgroups capability or be a member of all of the groups
+  I need to have the accessallgroups capability
 
   Background:
     Given the following "users" exist:
@@ -11,6 +11,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
       | noneditor1 | Non-editing teacher | 1 | noneditor1@example.com |
       | noneditor2 | Non-editing teacher | 2 | noneditor2@example.com |
       | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
     And the following "courses" exist:
       | fullname | shortname | category |
       | Course 1 | C1 | 0 |
@@ -20,6 +21,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
       | noneditor1 | C1 | teacher |
       | noneditor2 | C1 | teacher |
       | student1 | C1 | student |
+      | student2 | C1 | student |
     And the following "groups" exist:
       | name | course | idnumber |
       | Group A | C1 | G1 |
@@ -35,18 +37,22 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
       | noneditor2 | G1 |
       | noneditor2 | G2 |
       | student1 | G1 |
-      | student1 | G2 |
-    And I log in as "teacher1"
+      | student2 | G1 |
+      | student2 | G2 |
+    And the following "activities" exist:
+      | activity   | name                   | intro                         | course | idnumber     | groupmode |
+      | forum      | Standard forum name    | Standard forum description    | C1     | sepgroups    | 1         |
+
+  Scenario: Teacher with accessallgroups can view all groups
+    Given I log in as "teacher1"
     And I follow "Course 1"
-    And I turn editing mode on
-    And I add a "Forum" to section "1" and I fill the form with:
-      | Forum name | Standard forum name |
-      | Forum type | Standard forum for general use |
-      | Description | Standard forum description |
-      | Group mode | Separate groups |
-    And I log out
+    When I follow "Standard forum name"
+    Then the "Separate groups" select box should contain "All participants"
+    Then the "Separate groups" select box should contain "Group A"
+    Then the "Separate groups" select box should contain "Group B"
+    Then the "Separate groups" select box should contain "Group C"
 
-  Scenario: Teacher with accessallgroups can post in all groups
+  Scenario: Teacher with accessallgroups can select any group when posting
     Given I log in as "teacher1"
     And I follow "Course 1"
     And I follow "Standard forum name"
@@ -54,8 +60,158 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
     Then the "Group" select box should contain "All participants"
     And the "Group" select box should contain "Group A"
     And the "Group" select box should contain "Group B"
+    And the "Group" select box should contain "Group C"
     And I should see "Post a copy to all groups"
 
+  Scenario: Teacher with accessallgroups can post in groups they are a member of
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Standard forum name"
+    And I select "Group A" from the "Separate groups" singleselect
+    When I click on "Add a new discussion topic" "button"
+    Then I should see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Teacher 1 -> Group B  |
+      | Message | Teacher 1 -> Group B  |
+      # Change the group in the post form.
+      | Group   | Group B               |
+    And I press "Post to forum"
+    And I wait to be redirected
+    # We should be redirected to the group that we selected when posting.
+    And the field "Separate groups" matches value "Group B"
+    And I should see "Group B" in the "Teacher 1 -> Group B" "table_row"
+    And I should not see "Group A" in the "Teacher 1 -> Group B" "table_row"
+    And I should not see "Group C" in the "Teacher 1 -> Group B" "table_row"
+    # It should also be displayed under All participants
+    And I select "All participants" from the "Separate groups" singleselect
+    And I should see "Group B" in the "Teacher 1 -> Group B" "table_row"
+    And I should not see "Group A" in the "Teacher 1 -> Group B" "table_row"
+    And I should not see "Group C" in the "Teacher 1 -> Group B" "table_row"
+    # It should not be displayed in Groups A, or C.
+    And I select "Group A" from the "Separate groups" singleselect
+    And I should not see "Teacher 1 -> Group B"
+    And I select "Group C" from the "Separate groups" singleselect
+    And I should not see "Teacher 1 -> Group B"
+
+  Scenario: Teacher with accessallgroups can post in groups they are not a member of
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Standard forum name"
+    And I select "Group A" from the "Separate groups" singleselect
+    When I click on "Add a new discussion topic" "button"
+    Then I should see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Teacher 1 -> Group C  |
+      | Message | Teacher 1 -> Group C  |
+      | Group   | Group C               |
+    And I press "Post to forum"
+    And I wait to be redirected
+    # We should be redirected to the group that we selected when posting.
+    And the field "Separate groups" matches value "Group C"
+    # We redirect to the group posted in automatically.
+    And I should see "Group C" in the "Teacher 1 -> Group C" "table_row"
+    And I should not see "Group A" in the "Teacher 1 -> Group C" "table_row"
+    And I should not see "Group B" in the "Teacher 1 -> Group C" "table_row"
+    # It should also be displayed under All participants
+    And I select "All participants" from the "Separate groups" singleselect
+    And I should see "Group C" in the "Teacher 1 -> Group C" "table_row"
+    And I should not see "Group A" in the "Teacher 1 -> Group C" "table_row"
+    And I should not see "Group B" in the "Teacher 1 -> Group C" "table_row"
+    # It should not be displayed in Groups A, or B.
+    And I select "Group A" from the "Separate groups" singleselect
+    And I should not see "Teacher 1 -> Group C"
+    And I select "Group B" from the "Separate groups" singleselect
+    And I should not see "Teacher 1 -> Group C"
+
+  Scenario: Teacher with accessallgroups can post to all groups
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Standard forum name"
+    When I click on "Add a new discussion topic" "button"
+    And I set the following fields to these values:
+      | Subject                   | Teacher 1 -> Post to all  |
+      | Message                   | Teacher 1 -> Post to all  |
+      | Post a copy to all groups | 1                       |
+    And I press "Post to forum"
+    And I wait to be redirected
+    # Posting to all groups means that we should be redirected to the page we started from.
+    And the field "Separate groups" matches value "All participants"
+    And I select "Group A" from the "Separate groups" singleselect
+    Then I should see "Group A" in the "Teacher 1 -> Post to all" "table_row"
+    And I should not see "Group B" in the "Teacher 1 -> Post to all" "table_row"
+    And I should not see "Group C" in the "Teacher 1 -> Post to all" "table_row"
+    And I select "Group B" from the "Separate groups" singleselect
+    And I should see "Group B" in the "Teacher 1 -> Post to all" "table_row"
+    And I should not see "Group A" in the "Teacher 1 -> Post to all" "table_row"
+    And I should not see "Group C" in the "Teacher 1 -> Post to all" "table_row"
+    And I select "Group C" from the "Separate groups" singleselect
+    And I should see "Group C" in the "Teacher 1 -> Post to all" "table_row"
+    And I should not see "Group A" in the "Teacher 1 -> Post to all" "table_row"
+    And I should not see "Group B" in the "Teacher 1 -> Post to all" "table_row"
+    # No point testing the "All participants".
+
+  Scenario: Students in one group can only post in their group
+    Given I log in as "student1"
+    And I follow "Course 1"
+    When I follow "Standard forum name"
+    Then I should see "Group A"
+    And I click on "Add a new discussion topic" "button"
+    And I should see "Group A"
+    And I should not see "Group B"
+    And I should not see "Group C"
+    And I should not see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Student -> B |
+      | Message | Student -> B |
+    And I press "Post to forum"
+    And I wait to be redirected
+    And I should see "Group A" in the "Student -> B" "table_row"
+    And I should not see "Group B" in the "Student -> B" "table_row"
+
+  Scenario: Students in multiple group can post in all of their group individually
+    Given I log in as "student2"
+    And I follow "Course 1"
+    When I follow "Standard forum name"
+    And I select "Group A" from the "Separate groups" singleselect
+    And I click on "Add a new discussion topic" "button"
+    And the "Group" select box should not contain "All participants"
+    And the "Group" select box should contain "Group A"
+    And the "Group" select box should contain "Group B"
+    And the "Group" select box should not contain "Group C"
+    And I should not see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Student -> B  |
+      | Message | Student -> B  |
+      | Group   | Group B       |
+    And I press "Post to forum"
+    And I wait to be redirected
+    # We should be redirected to the group that we selected when posting.
+    And the field "Separate groups" matches value "Group B"
+    And I should see "Group B" in the "Student -> B" "table_row"
+    And I should not see "Group A" in the "Student -> B" "table_row"
+    And I select "Group A" from the "Separate groups" singleselect
+    And I should not see "Student -> B"
+    # Now try posting in Group A (starting at Group B)
+    And I select "Group B" from the "Separate groups" singleselect
+    And I click on "Add a new discussion topic" "button"
+    And the "Group" select box should not contain "All participants"
+    And the "Group" select box should contain "Group A"
+    And the "Group" select box should contain "Group B"
+    And the "Group" select box should not contain "Group C"
+    And I should not see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Student -> A  |
+      | Message | Student -> A  |
+      | Group   | Group A       |
+    And I press "Post to forum"
+    And I wait to be redirected
+    # We should be redirected to the group that we selected when posting.
+    And the field "Separate groups" matches value "Group A"
+    And I should see "Group A" in the "Student -> A" "table_row"
+    And I should not see "Group B" in the "Student -> A" "table_row"
+    And I select "Group B" from the "Separate groups" singleselect
+    And I should not see "Student -> A"
+
   Scenario: Teacher in all groups but without accessallgroups can only post in their groups
     And I log in as "admin"
     And I set the following system permissions of "Non-editing teacher" role:
index 7fda1f2..71e81a6 100644 (file)
@@ -64,7 +64,7 @@ Feature: Posting to groups in a separate group discussion when restricted to gro
     And I click on "Add a new discussion topic" "button"
     And the "Group" select box should contain "All participants"
     And the "Group" select box should contain "G2G1"
-    And I should see "Post a copy to all groups"
+    And I should not see "Post a copy to all groups"
 
   Scenario: Teacher in all groups but without accessallgroups can post in either group but not to All Participants
     And I log in as "admin"
diff --git a/mod/forum/tests/behat/visible_group_discussions.feature b/mod/forum/tests/behat/visible_group_discussions.feature
new file mode 100644 (file)
index 0000000..45cb568
--- /dev/null
@@ -0,0 +1,213 @@
+@mod @mod_forum
+Feature: Posting to all groups in a visible group discussion is restricted to users with access to all groups
+  In order to post to all groups in a forum with visible groups
+  As a teacher
+  I need to have the accessallgroups capability
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@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 |
+      | student2 | C1 | student |
+    And the following "groups" exist:
+      | name | course | idnumber |
+      | Group A | C1 | G1 |
+      | Group B | C1 | G2 |
+      | Group C | C1 | G3 |
+    And the following "group members" exist:
+      | user | group |
+      | teacher1 | G1 |
+      | teacher1 | G2 |
+      | student1 | G1 |
+      | student2 | G1 |
+      | student2 | G2 |
+    And the following "activities" exist:
+      | activity   | name                   | intro                         | course | idnumber     | groupmode |
+      | forum      | Standard forum name    | Standard forum description    | C1     | groups       | 2         |
+
+  Scenario: Teacher with accessallgroups can view all groups
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    When I follow "Standard forum name"
+    Then the "Visible groups" select box should contain "All participants"
+    Then the "Visible groups" select box should contain "Group A"
+    Then the "Visible groups" select box should contain "Group B"
+    Then the "Visible groups" select box should contain "Group C"
+
+  Scenario: Teacher with accessallgroups can select any group when posting
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Standard forum name"
+    When I click on "Add a new discussion topic" "button"
+    Then the "Group" select box should contain "All participants"
+    And the "Group" select box should contain "Group A"
+    And the "Group" select box should contain "Group B"
+    And the "Group" select box should contain "Group C"
+    And I should see "Post a copy to all groups"
+
+  Scenario: Teacher with accessallgroups can post in groups they are a member of
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Standard forum name"
+    And I select "Group A" from the "Visible groups" singleselect
+    When I click on "Add a new discussion topic" "button"
+    Then I should see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Teacher 1 -> Group B  |
+      | Message | Teacher 1 -> Group B  |
+      # Change the group in the post form.
+      | Group   | Group B               |
+    And I press "Post to forum"
+    And I wait to be redirected
+    # We should be redirected to the group that we selected when posting.
+    And the field "Visible groups" matches value "Group B"
+    And I should see "Group B" in the "Teacher 1 -> Group B" "table_row"
+    And I should not see "Group A" in the "Teacher 1 -> Group B" "table_row"
+    And I should not see "Group C" in the "Teacher 1 -> Group B" "table_row"
+    # It should also be displayed under All participants
+    And I select "All participants" from the "Visible groups" singleselect
+    And I should see "Group B" in the "Teacher 1 -> Group B" "table_row"
+    And I should not see "Group A" in the "Teacher 1 -> Group B" "table_row"
+    And I should not see "Group C" in the "Teacher 1 -> Group B" "table_row"
+    # It should not be displayed in Groups A, or C.
+    And I select "Group A" from the "Visible groups" singleselect
+    And I should not see "Teacher 1 -> Group B"
+    And I select "Group C" from the "Visible groups" singleselect
+    And I should not see "Teacher 1 -> Group B"
+
+  Scenario: Teacher with accessallgroups can post in groups they are not a member of
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Standard forum name"
+    And I select "Group A" from the "Visible groups" singleselect
+    When I click on "Add a new discussion topic" "button"
+    Then I should see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Teacher 1 -> Group C  |
+      | Message | Teacher 1 -> Group C  |
+      | Group   | Group C               |
+    And I press "Post to forum"
+    And I wait to be redirected
+    # We should be redirected to the group that we selected when posting.
+    And the field "Visible groups" matches value "Group C"
+    # We redirect to the group posted in automatically.
+    And I should see "Group C" in the "Teacher 1 -> Group C" "table_row"
+    And I should not see "Group A" in the "Teacher 1 -> Group C" "table_row"
+    And I should not see "Group B" in the "Teacher 1 -> Group C" "table_row"
+    # It should also be displayed under All participants
+    And I select "All participants" from the "Visible groups" singleselect
+    And I should see "Group C" in the "Teacher 1 -> Group C" "table_row"
+    And I should not see "Group A" in the "Teacher 1 -> Group C" "table_row"
+    And I should not see "Group B" in the "Teacher 1 -> Group C" "table_row"
+    # It should not be displayed in Groups A, or B.
+    And I select "Group A" from the "Visible groups" singleselect
+    And I should not see "Teacher 1 -> Group C"
+    And I select "Group B" from the "Visible groups" singleselect
+    And I should not see "Teacher 1 -> Group C"
+
+  Scenario: Teacher with accessallgroups can post to all groups
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Standard forum name"
+    When I click on "Add a new discussion topic" "button"
+    And I set the following fields to these values:
+      | Subject                   | Teacher 1 -> Post to all  |
+      | Message                   | Teacher 1 -> Post to all  |
+      | Post a copy to all groups | 1                       |
+    And I press "Post to forum"
+    And I wait to be redirected
+    # Posting to all groups means that we should be redirected to the page we started from.
+    And the field "Visible groups" matches value "All participants"
+    And I select "Group A" from the "Visible groups" singleselect
+    Then I should see "Group A" in the "Teacher 1 -> Post to all" "table_row"
+    And I should not see "Group B" in the "Teacher 1 -> Post to all" "table_row"
+    And I should not see "Group C" in the "Teacher 1 -> Post to all" "table_row"
+    And I select "Group B" from the "Visible groups" singleselect
+    And I should see "Group B" in the "Teacher 1 -> Post to all" "table_row"
+    And I should not see "Group A" in the "Teacher 1 -> Post to all" "table_row"
+    And I should not see "Group C" in the "Teacher 1 -> Post to all" "table_row"
+    And I select "Group C" from the "Visible groups" singleselect
+    And I should see "Group C" in the "Teacher 1 -> Post to all" "table_row"
+    And I should not see "Group A" in the "Teacher 1 -> Post to all" "table_row"
+    And I should not see "Group B" in the "Teacher 1 -> Post to all" "table_row"
+    # No point testing the "All participants".
+
+  Scenario: Students can view all groups
+    Given I log in as "student1"
+    And I follow "Course 1"
+    When I follow "Standard forum name"
+    Then the "Visible groups" select box should contain "All participants"
+    Then the "Visible groups" select box should contain "Group A"
+    Then the "Visible groups" select box should contain "Group B"
+    Then the "Visible groups" select box should contain "Group C"
+
+  Scenario: Students in one group can only post in their group
+    Given I log in as "student1"
+    And I follow "Course 1"
+    When I follow "Standard forum name"
+    Then I should see "Group A"
+    And I click on "Add a new discussion topic" "button"
+    And I should see "Group A"
+    And I should not see "Group B"
+    And I should not see "Group C"
+    And I should not see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Student -> B |
+      | Message | Student -> B |
+    And I press "Post to forum"
+    And I wait to be redirected
+    And I should see "Group A" in the "Student -> B" "table_row"
+    And I should not see "Group B" in the "Student -> B" "table_row"
+
+  Scenario: Students in multiple group can post in all of their group individually
+    Given I log in as "student2"
+    And I follow "Course 1"
+    When I follow "Standard forum name"
+    And I select "Group A" from the "Visible groups" singleselect
+    And I click on "Add a new discussion topic" "button"
+    And the "Group" select box should not contain "All participants"
+    And the "Group" select box should contain "Group A"
+    And the "Group" select box should contain "Group B"
+    And the "Group" select box should not contain "Group C"
+    And I should not see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Student -> B  |
+      | Message | Student -> B  |
+      | Group   | Group B       |
+    And I press "Post to forum"
+    And I wait to be redirected
+    # We should be redirected to the group that we selected when posting.
+    And the field "Visible groups" matches value "Group B"
+    And I should see "Group B" in the "Student -> B" "table_row"
+    And I should not see "Group A" in the "Student -> B" "table_row"
+    And I select "Group A" from the "Visible groups" singleselect
+    And I should not see "Student -> B"
+    # Now try posting in Group A (starting at Group B)
+    And I select "Group B" from the "Visible groups" singleselect
+    And I click on "Add a new discussion topic" "button"
+    And the "Group" select box should not contain "All participants"
+    And the "Group" select box should contain "Group A"
+    And the "Group" select box should contain "Group B"
+    And the "Group" select box should not contain "Group C"
+    And I should not see "Post a copy to all groups"
+    And I set the following fields to these values:
+      | Subject | Student -> A  |
+      | Message | Student -> A  |
+      | Group   | Group A       |
+    And I press "Post to forum"
+    And I wait to be redirected
+    # We should be redirected to the group that we selected when posting.
+    And the field "Visible groups" matches value "Group A"
+    And I should see "Group A" in the "Student -> A" "table_row"
+    And I should not see "Group B" in the "Student -> A" "table_row"
+    And I select "Group B" from the "Visible groups" singleselect
+    And I should not see "Student -> A"
index c663974..884f497 100644 (file)
@@ -275,12 +275,12 @@ $string['maximumnumberofanswersbranches_help'] = 'This setting specifies the max
 $string['maximumnumberofattempts'] = 'Maximum number of attempts';
 $string['maximumnumberofattempts_help'] = 'This setting specifies the maximum number of attempts allowed for each question. If answered incorrectly repeatedly, when the maximum is reached, the next page of the lesson is displayed.';
 $string['maximumnumberofattemptsreached'] = 'Maximum number of attempts reached - Moving to next page';
-$string['mediaclose'] = 'Show close button:';
+$string['mediaclose'] = 'Show close button';
 $string['mediafile'] = 'File pop-up';
 $string['mediafile_help'] = 'To include a pop-up window at the beginning of a lesson, choose the appropriate file to display. Every lesson page will include a link to re-open the pop-up if necessary.';
 $string['mediafilepopup'] = 'Click here to view';
-$string['mediaheight'] = 'Popup window height:';
-$string['mediawidth'] = 'Popup window width:';
+$string['mediaheight'] = 'Popup window height';
+$string['mediawidth'] = 'Popup window width';
 $string['messageprovider:graded_essay'] = 'Lesson essay graded notification';
 $string['minimumnumberofquestions'] = 'Minimum number of questions';
 $string['minimumnumberofquestions_help'] = 'This setting specifies the minimum number of questions that will be used to calculate a grade for the activity. If the lesson contains one or more content pages, the minimum number of questions should be set to zero.
index 7385e88..5d3eb3a 100644 (file)
@@ -336,6 +336,12 @@ class mod_lesson_mod_form extends moodleform_mod {
     function validation($data, $files) {
         $errors = parent::validation($data, $files);
 
+        // Check open and close times are consistent.
+        if ($data['available'] != 0 && $data['deadline'] != 0 &&
+                $data['deadline'] < $data['available']) {
+            $errors['deadline'] = get_string('closebeforeopen', 'lesson');
+        }
+
         if (!empty($data['usepassword']) && empty($data['password'])) {
             $errors['password'] = get_string('emptypassword', 'lesson');
         }
index 105213e..5060db3 100644 (file)
@@ -788,16 +788,13 @@ class quiz_attempt {
     }
 
     /**
-     * Is this a student dealing with their own attempt/teacher previewing,
-     * or someone with 'mod/quiz:viewreports' reviewing someone elses attempt.
+     * Is this someone dealing with their own attempt or preview?
      *
-     * @return bool whether this situation should be treated as someone looking at their own
-     * attempt. The distinction normally only matters when an attempt is being reviewed.
+     * @return bool true => own attempt/preview. false => reviewing someone elses.
      */
     public function is_own_attempt() {
         global $USER;
-        return $this->attempt->userid == $USER->id &&
-                (!$this->is_preview_user() || $this->attempt->preview);
+        return $this->attempt->userid == $USER->id;
     }
 
     /**
@@ -805,7 +802,7 @@ class quiz_attempt {
      */
     public function is_own_preview() {
         global $USER;
-        return $this->attempt->userid == $USER->id &&
+        return $this->is_own_attempt() &&
                 $this->is_preview_user() && $this->attempt->preview;
     }
 
@@ -961,6 +958,11 @@ class quiz_attempt {
             if (is_null($this->reviewoptions)) {
                 $this->reviewoptions = quiz_get_review_options($this->get_quiz(),
                         $this->attempt, $this->quizobj->get_context());
+                if ($this->is_own_preview()) {
+                    // It should  always be possible for a teacher to review their
+                    // own preview irrespective of the review options settings.
+                    $this->reviewoptions->attempt = true;
+                }
             }
             return $this->reviewoptions;
 
@@ -1569,7 +1571,19 @@ class quiz_attempt {
      */
     public function check_file_access($slot, $reviewing, $contextid, $component,
             $filearea, $args, $forcedownload) {
-        return $this->quba->check_file_access($slot, $this->get_display_options($reviewing),
+        $options = $this->get_display_options($reviewing);
+
+        // Check permissions - warning there is similar code in review.php and
+        // reviewquestion.php. If you change on, change them all.
+        if ($reviewing && $this->is_own_attempt() && !$options->attempt) {
+            return false;
+        }
+
+        if ($reviewing && !$this->is_own_attempt() && !$this->is_review_allowed()) {
+            return false;
+        }
+
+        return $this->quba->check_file_access($slot, $options,
                 $component, $filearea, $args, $forcedownload);
     }
 
index b923799..eebab6b 100644 (file)
@@ -61,7 +61,8 @@ $accessmanager->setup_attempt_page($PAGE);
 
 $options = $attemptobj->get_display_options(true);
 
-// Check permissions.
+// Check permissions - warning there is similar code in reviewquestion.php and
+// quiz_attempt::check_file_access. If you change on, change them all.
 if ($attemptobj->is_own_attempt()) {
     if (!$attemptobj->is_finished()) {
         redirect($attemptobj->attempt_url(null, $page));
@@ -91,7 +92,7 @@ if ($options->flags == question_display_options::EDITABLE && optional_param('sav
 }
 
 // Work out appropriate title and whether blocks should be shown.
-if ($attemptobj->is_preview_user() && $attemptobj->is_own_attempt()) {
+if ($attemptobj->is_own_preview()) {
     $strreviewtitle = get_string('reviewofpreview', 'quiz');
     navigation_node::override_active_url($attemptobj->start_attempt_url());
 
index 0420768..e7bcf91 100644 (file)
@@ -52,7 +52,8 @@ $PAGE->set_pagelayout('popup');
 $PAGE->set_heading($attemptobj->get_course()->fullname);
 $output = $PAGE->get_renderer('mod_quiz');
 
-// Check permissions.
+// Check permissions - warning there is similar code in review.php and
+// quiz_attempt::check_file_access. If you change on, change them all.
 if ($attemptobj->is_own_attempt()) {
     if (!$attemptobj->is_finished()) {
         echo $output->review_question_not_allowed(get_string('cannotreviewopen', 'quiz'));
index b69e374..405d714 100644 (file)
@@ -6,82 +6,52 @@ Feature: Set a quiz to be marked complete when the student uses all attempts all
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email |
-      | student1 | Student | 1 | student1@example.com |
-      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | 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 |
+      | fullname | shortname | category | enablecompletion |
+      | Course 1 | C1        | 0        | 1                |
     And the following "course enrolments" exist:
-      | user | course | role |
-      | teacher1 | C1 | editingteacher |
-      | student1 | C1 | student |
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
     And the following config values are set as admin:
-     | enablecompletion | 1 |
-    And I log in as "admin"
-    And I navigate to "Grade item settings" node in "Site administration > Grades"
-    And I set the field "Advanced grade item options" to "hiddenuntil"
-    And I press "Save changes"
-    And I log out
+      | enablecompletion    | 1           |
+      | grade_item_advanced | hiddenuntil |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype     | name           | questiontext              |
+      | Test questions   | truefalse | First question | Answer the first question |
+    And the following "activities" exist:
+      | activity   | name           | course | idnumber | attempts | gradepass | completion | completionattemptsexhausted |
+      | quiz       | Test quiz name | C1     | quiz1    | 2        | 5.00      | 2          | 1                           |
+    And quiz "Test quiz name" contains the following questions:
+      | question       | page |
+      | First question | 1    |
 
   Scenario: student1 uses up both attempts without passing
-    When I log in as "teacher1"
-    And I follow "Course 1"
-    And I turn editing mode on
-    And I click on "Edit settings" "link" in the "Administration" "block"
-    And I set the following fields to these values:
-      | Enable completion tracking | Yes |
-    And I press "Save and display"
-    And I add a "Quiz" to section "1" and I fill the form with:
-      | Name        | Test quiz name        |
-      | Description | Test quiz description |
-      | Completion tracking | Show activity as complete when conditions are met |
-      | Attempts allowed | 2 |
-      | Require passing grade | 1 |
-      | Or all available attempts completed | 1 |
-    And I add a "True/False" question to the "Test quiz name" quiz with:
-      | Question name                      | First question                          |
-      | Question text                      | Answer the first question               |
-      | General feedback                   | Thank you, this is the general feedback |
-      | Correct answer                     | True                                    |
-      | Feedback for the response 'True'.  | So you think it is true                 |
-      | Feedback for the response 'False'. | So you think it is false                |
-    And I follow "Course 1"
-    And I navigate to "Grades" node in "Course administration"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
-    And I follow "Edit  quiz Test quiz name"
-    Then I should see "Edit grade item"
-    And I set the field "gradepass" to "5"
-    And I press "Save changes"
-    And I should see "Categories and items"
-    Then I log out
-
-    And I log in as "student1"
+    When I log in as "student1"
     And I follow "Course 1"
     And "//img[contains(@alt, 'Not completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element"
     And I follow "Test quiz name"
     And I press "Attempt quiz now"
-    And I should see "Question 1"
-    And I should see "Answer the first question"
     And I set the field "False" to "1"
     And I press "Next"
-    And I should see "Answer saved"
     And I press "Submit all and finish"
     And I follow "C1"
     And "//img[contains(@alt, 'Not completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element"
     And I follow "Test quiz name"
     And I press "Re-attempt quiz"
-    Then I should see "Question 1"
-    And I should see "Answer the first question"
     And I set the field "False" to "1"
     And I press "Next"
-    And I should see "Answer saved"
     And I press "Submit all and finish"
     And I follow "C1"
-    And "//img[contains(@alt, 'Completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element"
+    Then "//img[contains(@alt, 'Completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element"
     And I log out
     And I log in as "teacher1"
     And I follow "Course 1"
-    And I follow "Activity completion"
-    Then "//img[contains(@title,'Test quiz name') and @alt='Completed']" "xpath_element" should exist in the "Student 1" "table_row"
-
+    And I navigate to "Activity completion" node in "Course administration > Reports"
+    And "//img[contains(@title,'Test quiz name') and @alt='Completed']" "xpath_element" should exist in the "Student 1" "table_row"
index 3ae54f2..ba735c1 100644 (file)
@@ -6,81 +6,45 @@ Feature: Set a quiz to be marked complete when the student passes
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email |
-      | student1 | Student | 1 | student1@example.com |
-      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | 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 |
+      | fullname | shortname | category | enablecompletion |
+      | Course 1 | C1        | 0        | 1                |
     And the following "course enrolments" exist:
-      | user | course | role |
-      | teacher1 | C1 | editingteacher |
-      | student1 | C1 | student |
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
     And the following config values are set as admin:
-     | enablecompletion | 1 |
-    And I log in as "admin"
-    And I navigate to "Grade item settings" node in "Site administration > Grades"
-    And I set the field "Advanced grade item options" to "hiddenuntil"
-    And I press "Save changes"
-    And I log out
+      | enablecompletion    | 1           |
+      | grade_item_advanced | hiddenuntil |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype     | name           | questiontext              |
+      | Test questions   | truefalse | First question | Answer the first question |
+    And the following "activities" exist:
+      | activity   | name           | course | idnumber | attempts | gradepass | completion | completionpass |
+      | quiz       | Test quiz name | C1     | quiz1    | 4        | 5.00      | 2          | 1              |
+    And quiz "Test quiz name" contains the following questions:
+      | question       | page |
+      | First question | 1    |
 
   Scenario: student1 passes on the first try
-    When I log in as "teacher1"
-    And I follow "Course 1"
-    And I turn editing mode on
-    And I click on "Edit settings" "link" in the "Administration" "block"
-    And I set the following fields to these values:
-      | Enable completion tracking | Yes |
-    And I press "Save and display"
-    And I add a "Quiz" to section "1" and I fill the form with:
-      | Name        | Test quiz name        |
-      | Description | Test quiz description |
-      | Completion tracking | Show activity as complete when conditions are met |
-      | Attempts allowed | 4 |
-      | Require passing grade | 1 |
-    And I add a "True/False" question to the "Test quiz name" quiz with:
-      | Question name                      | First question                          |
-      | Question text                      | Answer the first question               |
-      | General feedback                   | Thank you, this is the general feedback |
-      | Correct answer                     | True                                    |
-      | Feedback for the response 'True'.  | So you think it is true                 |
-      | Feedback for the response 'False'. | So you think it is false                |
-    And I follow "Course 1"
-    And I navigate to "Grades" node in "Course administration"
-    And I set the field "jump" to "Categories and items"
-    And I press "Go"
-    And I follow "Edit  quiz Test quiz name"
-    Then I should see "Edit grade item"
-    And I set the field "gradepass" to "5"
-    And I press "Save changes"
-    Then I should see "Categories and items"
-    And I log out
-
-    And I log in as "student1"
+    When I log in as "student1"
     And I follow "Course 1"
     And "//img[contains(@alt, 'Not completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element"
     And I follow "Test quiz name"
     And I press "Attempt quiz now"
-    Then I should see "Question 1"
-    And I should see "Answer the first question"
-    And I set the field "False" to "1"
-    And I press "Next"
-    And I should see "Answer saved"
-    And I press "Submit all and finish"
-    And I follow "C1"
-    And "//img[contains(@alt, 'Not completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element"
-    And I follow "Test quiz name"
-    And I press "Re-attempt quiz"
-    Then I should see "Question 1"
-    And I should see "Answer the first question"
     And I set the field "True" to "1"
     And I press "Next"
-    And I should see "Answer saved"
     And I press "Submit all and finish"
     And I follow "C1"
-    And "//img[contains(@alt, 'Completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element"
+    Then "//img[contains(@alt, 'Completed: Test quiz name')]" "xpath_element" should exist in the "li.modtype_quiz" "css_element"
     And I log out
     And I log in as "teacher1"
     And I follow "Course 1"
-    And I follow "Activity completion"
-    Then "//img[contains(@title,'Test quiz name') and @alt='Completed']" "xpath_element" should exist in the "Student 1" "table_row"
+    And I navigate to "Activity completion" node in "Course administration > Reports"
+    And "//img[contains(@title,'Test quiz name') and @alt='Completed']" "xpath_element" should exist in the "Student 1" "table_row"
diff --git a/mod/quiz/tests/behat/manually_mark_question.feature b/mod/quiz/tests/behat/manually_mark_question.feature
new file mode 100644 (file)
index 0000000..dba9652
--- /dev/null
@@ -0,0 +1,57 @@
+@mod @mod_quiz
+Feature: Teachers can override the grade for any question
+  As a teacher
+  In order to correct errors
+  I must be able to override the grades that Moodle gives.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | student1 | Student   | 1        | student0@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 the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name  | questiontext    | defaultmark |
+      | Test questions   | essay       | TF1   | First question  | 20          |
+    And the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | grade |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | 20    |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page |
+      | TF1      | 1    |
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    And I follow "Finish attempt ..."
+    And I press "Submit all and finish"
+    And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Attempts: 1"
+    And I follow "Review attempt"
+
+  @javascript
+  Scenario: Validating the marking of an essay question attempt.
+    When I follow "Make comment or override mark"
+    And I switch to "commentquestion" window
+    And I set the field "Mark" to "25"
+    And I press "Save"
+    Then I should see "This grade is outside the valid range."
+    And I set the field "Mark" to "aa"
+    And I press "Save"
+    And I should see "That is not a valid number."
+    And I set the field "Mark" to "10.0"
+    And I press "Save"
+    And I should see "Changes saved"
index 7546d11..8e3221b 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js and b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js differ
index deb7b51..a766e4b 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js and b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js differ
index 7546d11..8e3221b 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js and b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js differ
index b3f81bf..ec32959 100644 (file)
@@ -177,7 +177,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
                     Y.Moodle.mod_quiz.util.slot.remove(element);
                     this.reorganise_edit_page();
                     if (M.core.actionmenu && M.core.actionmenu.instance) {
-                        M.core.actionmenu.instance.hideMenu();
+                        M.core.actionmenu.instance.hideMenu(ev);
                     }
                 }
             });
@@ -219,7 +219,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
 
         this.send_request(data, null, function(response) {
             if (M.core.actionmenu && M.core.actionmenu.instance) {
-                M.core.actionmenu.instance.hideMenu();
+                M.core.actionmenu.instance.hideMenu(ev);
             }
 
             // Try to retrieve the existing string from the server.
index ecc34a5..bd7897b 100644 (file)
@@ -188,7 +188,7 @@ $string['identifier'] = 'Question identifier';
 $string['incomplete'] = 'Incomplete';
 $string['info'] = 'Info';
 $string['interactions'] = 'Interactions';
-$string['repositorynotsupported'] = 'Only file system repositories are supported when linking directly to an imsmanifest.xml file.';
+$string['repositorynotsupported'] = 'This repository does not support linking directly to an imsmanifest.xml file.';
 $string['trackid'] = 'Id';
 $string['trackid_help'] = 'This is the identifier set by your SCORM package for this question, the SCORM specification doesn\'t allow the full question text to be provided.';
 $string['trackcorrectcount'] = 'Correct count';
index 2eaa8ae..9a93e91 100644 (file)
@@ -3,7 +3,11 @@ information provided here is intended especially for developers.
 
 === 3.0 ===
 
-* Function scorm_view_display was renamed to scorm_print_launch to avoid confussion with new function scorm_view.
+* Dropped support for the $module in mod/xxx/version.php files (deprecated
+  since 2.7). All activity modules must use the $plugin syntax now. See
+  https://docs.moodle.org/dev/version.php for details (MDL-43896).
+* Function scorm_view_display was renamed to scorm_print_launch to avoid
+  confussion with new function scorm_view.
 
 === 2.9 ===
 
index 8daaab0..d73328d 100644 (file)
@@ -204,7 +204,7 @@ abstract class question_behaviour {
 
         $vars = array('comment' => PARAM_RAW, 'commentformat' => PARAM_INT);
         if ($this->qa->get_max_mark()) {
-            $vars['mark'] = question_attempt::PARAM_MARK;
+            $vars['mark'] = PARAM_RAW_TRIMMED;
             $vars['maxmark'] = PARAM_FLOAT;
         }
         return $vars;
@@ -477,15 +477,25 @@ abstract class question_behaviour {
         }
 
         if ($pendingstep->has_behaviour_var('mark')) {
-            $fraction = $pendingstep->get_behaviour_var('mark') /
-                            $pendingstep->get_behaviour_var('maxmark');
-            if ($pendingstep->get_behaviour_var('mark') === '') {
+            $mark = question_utils::clean_param_mark($pendingstep->get_behaviour_var('mark'));
+            if ($mark === null) {
+                throw new coding_exception('Inalid number format ' . $pendingstep->get_behaviour_var('mark') .
+                        ' when processing a manual grading action.', 'Question ' . $this->question->id .
+                        ', slot ' . $this->qa->get_slot());
+
+            } else if ($mark === '') {
                 $fraction = null;
-            } else if ($fraction > $this->qa->get_max_fraction() || $fraction < $this->qa->get_min_fraction()) {
-                throw new coding_exception('Score out of range when processing ' .
-                        'a manual grading action.', 'Question ' . $this->question->id .
-                                ', slot ' . $this->qa->get_slot() . ', fraction ' . $fraction);
+
+            } else {
+                $fraction = $pendingstep->get_behaviour_var('mark') /
+                        $pendingstep->get_behaviour_var('maxmark');
+                if ($fraction > $this->qa->get_max_fraction() || $fraction < $this->qa->get_min_fraction()) {
+                    throw new coding_exception('Score out of range when processing ' .
+                            'a manual grading action.', 'Question ' . $this->question->id .
+                            ', slot ' . $this->qa->get_slot() . ', fraction ' . $fraction);
+                }
             }
+
             $pendingstep->set_fraction($fraction);
         }
 
index 0e2aec8..9566047 100644 (file)
@@ -384,4 +384,98 @@ class qbehaviour_manualgraded_walkthrough_testcase extends qbehaviour_walkthroug
         $this->displayoptions->manualcomment = question_display_options::VISIBLE;
         $this->check_output_contains('This should only appear if the displya options allow it');
     }
+
+    public function test_manual_graded_invalid_value_throws_exception() {
+        global $PAGE;
+
+        // The current text editor depends on the users profile setting - so it needs a valid user.
+        $this->setAdminUser();
+        // Required to init a text editor.
+        $PAGE->set_url('/');
+
+        // Create an essay question.
+        $essay = test_question_maker::make_an_essay_question();
+        $this->start_attempt_at_question($essay, 'deferredfeedback', 10);
+
+        // Check the right model is being used.
+        $this->assertEquals('manualgraded', $this->quba->get_question_attempt(
+                $this->slot)->get_behaviour_name());
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output($this->get_contains_question_text_expectation($essay),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Simulate some data submitted by the student.
+        $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                new question_contains_tag_with_attribute('textarea', 'name',
+                $this->quba->get_question_attempt($this->slot)->get_qt_field_name('answer')),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$needsgrading);
+        $this->check_current_mark(null);
+        $this->assertEquals('This is my wonderful essay!',
+                $this->quba->get_response_summary($this->slot));
+
+        // Try to process a an invalid grade.
+        $this->setExpectedException('coding_exception');
+        $this->manual_grade('Comment', 'frog', FORMAT_HTML);
+    }
+
+    public function test_manual_graded_out_of_range_throws_exception() {
+        global $PAGE;
+
+        // The current text editor depends on the users profile setting - so it needs a valid user.
+        $this->setAdminUser();
+        // Required to init a text editor.
+        $PAGE->set_url('/');
+
+        // Create an essay question.
+        $essay = test_question_maker::make_an_essay_question();
+        $this->start_attempt_at_question($essay, 'deferredfeedback', 10);
+
+        // Check the right model is being used.
+        $this->assertEquals('manualgraded', $this->quba->get_question_attempt(
+                $this->slot)->get_behaviour_name());
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output($this->get_contains_question_text_expectation($essay),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Simulate some data submitted by the student.
+        $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                new question_contains_tag_with_attribute('textarea', 'name',
+                $this->quba->get_question_attempt($this->slot)->get_qt_field_name('answer')),
+                $this->get_does_not_contain_feedback_expectation());
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Verify.
+        $this->check_current_state(question_state::$needsgrading);
+        $this->check_current_mark(null);
+        $this->assertEquals('This is my wonderful essay!',
+                $this->quba->get_response_summary($this->slot));
+
+        // Try to process a an invalid grade.
+        $this->setExpectedException('coding_exception');
+        $this->manual_grade('Comment', '10.1', FORMAT_HTML);
+    }
 }
index 0397da0..41f58b2 100644 (file)
@@ -82,6 +82,7 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
 
         $commenttext = format_text($commenttext, $commentformat, array('para' => false));
 
+        $editor->set_text($commenttext);
         $editor->use_editor($id, array('context' => $options->context));
 
         $commenteditor = html_writer::tag('div', html_writer::tag('textarea', s($commenttext),
@@ -125,8 +126,7 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
                 'id'=> $markfield
             );
             if (!is_null($currentmark)) {
-                $attributes['value'] = $qa->format_fraction_as_mark(
-                        $currentmark / $maxmark, $options->markdp);
+                $attributes['value'] = $currentmark;
             }
             $a = new stdClass();
             $a->max = $qa->format_max_mark($options->markdp);
@@ -146,11 +146,11 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
                 'value' => $qa->get_max_fraction(),
             ));
 
+            $error = $qa->validate_manual_mark($currentmark);
             $errorclass = '';
-            $error = '';
-            if ($currentmark > $maxmark * $qa->get_max_fraction() || $currentmark < $maxmark * $qa->get_min_fraction()) {
-                $errorclass = ' error';
-                $error = html_writer::tag('span', get_string('manualgradeoutofrange', 'question'),
+            if ($error !== '') {
+                $erroclass = ' error';
+                $error = html_writer::tag('span', $error,
                         array('class' => 'error')) . html_writer::empty_tag('br');
             }
 
index a33d4bc..45c4a7f 100644 (file)
@@ -143,7 +143,8 @@ abstract class question_engine {
         $maxmark = optional_param($prefix . '-maxmark', null, PARAM_FLOAT);
         $minfraction = optional_param($prefix . ':minfraction', null, PARAM_FLOAT);
         $maxfraction = optional_param($prefix . ':maxfraction', null, PARAM_FLOAT);
-        return is_null($mark) || ($mark >= $minfraction * $maxmark && $mark <= $maxfraction * $maxmark);
+        return $mark === '' ||
+                ($mark !== null && $mark >= $minfraction * $maxmark && $mark <= $maxfraction * $maxmark);
     }
 
     /**
@@ -904,8 +905,9 @@ abstract class question_utils {
     /**
      * Typically, $mark will have come from optional_param($name, null, PARAM_RAW_TRIMMED).
      * This method copes with:
-     *  - keeping null or '' input unchanged.
-     *  - nubmers that were typed as either 1.00 or 1,00 form.
+     *  - keeping null or '' input unchanged - important to let teaches set a question back to requries grading.
+     *  - numbers that were typed as either 1.00 or 1,00 form.
+     *  - invalid things, which get turned into null.
      *
      * @param string|null $mark raw use input of a mark.
      * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
@@ -915,7 +917,13 @@ abstract class question_utils {
             return $mark;
         }
 
-        return clean_param(str_replace(',', '.', $mark), PARAM_FLOAT);
+        $mark = str_replace(',', '.', $mark);
+        // This regexp should match the one in validate_param.
+        if (!preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', $mark)) {
+            return null;
+        }
+
+        return clean_param($mark, PARAM_FLOAT);
     }
 
     /**
index bff43e8..bd25280 100644 (file)
@@ -49,10 +49,10 @@ class question_attempt {
     const USE_RAW_DATA = 'use raw data';
 
     /**
-     * @var string special value used by manual grading because {@link PARAM_FLOAT}
-     * converts '' to 0.
+     * @var string Should not longer be used.
+     * @deprecated since Moodle 3.0
      */
-    const PARAM_MARK = 'parammark';
+    const PARAM_MARK = PARAM_RAW_TRIMMED;
 
     /**
      * @var string special value to indicate a response variable that is uploaded
@@ -651,13 +651,12 @@ class question_attempt {
      * This is used by the manual grading code, particularly in association with
      * validation. If there is a mark submitted in the request, then use that,
      * otherwise use the latest mark for this question.
-     * @return number the current mark for this question.
-     * {@link get_fraction()} * {@link get_max_mark()}.
+     * @return number the current manual mark for this question, formatted for display.
      */
     public function get_current_manual_mark() {
-        $mark = $this->get_submitted_var($this->get_behaviour_field_name('mark'), question_attempt::PARAM_MARK);
+        $mark = $this->get_submitted_var($this->get_behaviour_field_name('mark'), PARAM_RAW_TRIMMED);
         if (is_null($mark)) {
-            return $this->get_mark();
+            return format_float($this->get_mark(), 7, true, true);
         } else {
             return $mark;
         }
@@ -1030,9 +1029,6 @@ class question_attempt {
      */
     public function get_submitted_var($name, $type, $postdata = null) {
         switch ($type) {
-            case self::PARAM_MARK:
-                // Special case to work around PARAM_FLOAT converting '' to 0.
-                return question_utils::clean_param_mark($this->get_submitted_var($name, PARAM_RAW_TRIMMED, $postdata));
 
             case self::PARAM_FILES:
                 return $this->process_response_files($name, $name, $postdata);
@@ -1054,6 +1050,29 @@ class question_attempt {
         }
     }
 
+    /**
+     * Validate the manual mark for a question.
+     * @param unknown $currentmark the user input (e.g. '1,0', '1,0' or 'invalid'.
+     * @return string any errors with the value, or '' if it is OK.
+     */
+    public function validate_manual_mark($currentmark) {
+        if ($currentmark === null || $currentmark === '') {
+            return '';
+        }
+
+        $mark = question_utils::clean_param_mark($currentmark);
+        if ($mark === null) {
+            return get_string('manualgradeinvalidformat', 'question');
+        }
+
+        $maxmark = $this->get_max_mark();
+        if ($mark > $maxmark * $this->get_max_fraction() || $mark < $maxmark * $this->get_min_fraction()) {
+            return get_string('manualgradeoutofrange', 'question');
+        }
+
+        return '';
+    }
+
     /**
      * Handle a submitted variable representing uploaded files.
      * @param string $name the field name.
index 21f6ad9..3469ec6 100644 (file)
@@ -109,36 +109,6 @@ class question_attempt_testcase extends advanced_testcase {
                 'reallyunlikelyvariablename', PARAM_BOOL));
     }
 
-    public function test_get_submitted_var_param_mark_not_present() {
-        $this->assertNull($this->qa->get_submitted_var(
-                'name', question_attempt::PARAM_MARK, array()));
-    }
-
-    public function test_get_submitted_var_param_mark_blank() {
-        $this->assertSame('', $this->qa->get_submitted_var(
-                'name', question_attempt::PARAM_MARK, array('name' => '')));
-    }
-
-    public function test_get_submitted_var_param_mark_number() {
-        $this->assertSame(123.0, $this->qa->get_submitted_var(
-                'name', question_attempt::PARAM_MARK, array('name' => '123')));
-    }
-
-    public function test_get_submitted_var_param_mark_number_uk_decimal() {
-        $this->assertSame(123.45, $this->qa->get_submitted_var(
-                'name', question_attempt::PARAM_MARK, array('name' => '123.45')));
-    }
-
-    public function test_get_submitted_var_param_mark_number_eu_decimal() {
-        $this->assertSame(123.45, $this->qa->get_submitted_var(
-                'name', question_attempt::PARAM_MARK, array('name' => '123,45')));
-    }
-
-    public function test_get_submitted_var_param_mark_invalid() {
-        $this->assertSame(0.0, $this->qa->get_submitted_var(
-                'name', question_attempt::PARAM_MARK, array('name' => 'frog')));
-    }
-
     public function test_get_all_submitted_qt_vars() {
         $this->qa->set_usage_id('MDOgzdhS4W');
         $this->qa->set_slot(1);
index b5f1d4b..a5cddbd 100644 (file)
@@ -165,4 +165,20 @@ class question_attempt_with_steps_test extends advanced_testcase {
         $this->setExpectedException('moodle_exception');
         $qa->get_max_fraction();
     }
+
+    public function test_validate_manual_mark() {
+        $this->qa->set_min_fraction(0);
+        $this->qa->set_max_fraction(1);
+        $this->assertSame('', $this->qa->validate_manual_mark(null));
+        $this->assertSame('', $this->qa->validate_manual_mark(''));
+        $this->assertSame('', $this->qa->validate_manual_mark('0'));
+        $this->assertSame('', $this->qa->validate_manual_mark('0.0'));
+        $this->assertSame('', $this->qa->validate_manual_mark('2,0'));
+        $this->assertSame(get_string('manualgradeinvalidformat', 'question'),
+                $this->qa->validate_manual_mark('frog'));
+        $this->assertSame(get_string('manualgradeoutofrange', 'question'),
+                $this->qa->validate_manual_mark('2.1'));
+        $this->assertSame(get_string('manualgradeoutofrange', 'question'),
+                $this->qa->validate_manual_mark('-0,01'));
+    }
 }
index e5935f4..ab4559c 100644 (file)
@@ -194,6 +194,7 @@ class question_utils_test extends advanced_testcase {
 
     public function test_clean_param_mark() {
         $this->assertNull(question_utils::clean_param_mark(null));
+        $this->assertNull(question_utils::clean_param_mark('frog'));
         $this->assertSame('', question_utils::clean_param_mark(''));
         $this->assertSame(0.0, question_utils::clean_param_mark('0'));
         $this->assertSame(1.5, question_utils::clean_param_mark('1.5'));
index 86f1529..09c9d79 100644 (file)
@@ -1,5 +1,11 @@
 This files describes API changes for the core question engine.
 
+=== 3.0, 2.9.2, 2.8.8 ===
+
+1) The extra internal PARAM constant question_attempt::PARAM_MARK should no
+   longer be used. (It should not have been used outside the core of the
+   question system). See MDL-51090 if you want more explanation.
+
 
 === 2.9 ===
 
index 222e2a4..f8db551 100644 (file)
@@ -236,6 +236,7 @@ class qtype_essay_format_editor_renderer extends plugin_renderer_base {
         list($draftitemid, $response) = $this->prepare_response_for_editing(
                 $name, $step, $context);
 
+        $editor->set_text($response);
         $editor->use_editor($id, $this->get_editor_options($context),
                 $this->get_filepicker_options($context, $draftitemid));
 
index 582be83..66d5ef5 100644 (file)
@@ -26,9 +26,9 @@
 $string['apierror'] = '{$a}';
 $string['apikey'] = 'API key';
 $string['information'] = 'Get a <a href="https://developers.google.com/youtube/v3/getting-started">Google API key</a> for your Moodle site.';
-$string['pluginname'] = 'Youtube videos';
+$string['pluginname'] = 'YouTube videos';
 $string['search'] = 'Search videos';
-$string['youtube:view'] = 'Use youtube in file picker';
+$string['youtube:view'] = 'Use YouTube in file picker';
 $string['configplugin'] = 'YouTube repository type configuration';
 $string['sortby'] = 'Sort By';
 $string['sortpublished'] = 'Date Published';
index 76e98fa..5e2ea35 100644 (file)
@@ -315,14 +315,16 @@ function coursetag_get_tagged_courses($tagid) {
 function coursetag_delete_course_tags($courseid, $showfeedback=false) {
     global $DB, $OUTPUT;
 
-    if ($taginstances = $DB->get_fieldset_select('tag_instance', 'tagid', "itemtype = 'course' AND itemid = :courseid",
-        array('courseid' => $courseid))) {
+    if ($taginstances = $DB->get_recordset_select('tag_instance', "itemtype = 'course' AND itemid = :courseid",
+        array('courseid' => $courseid), '', 'tagid, tiuserid')) {
 
-        tag_delete(array_values($taginstances));
+        foreach ($taginstances as $record) {
+            tag_delete_instance('course', $courseid, $record->tagid, $record->tiuserid);
+        }
+        $taginstances->close();
     }
 
     if ($showfeedback) {
         echo $OUTPUT->notification(get_string('deletedcoursetags', 'tag'), 'notifysuccess');
     }
 }
-
index 102704c..2ab484b 100644 (file)
@@ -546,10 +546,8 @@ function tag_get_related_tags($tagid, $type=TAG_RELATED_ALL, $limitnum=10) {
 
     if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_CORRELATED ) {
         //gets the correlated