Merge branch 'MDL-49185-master' of git://github.com/lameze/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 7 Jul 2015 07:54:42 +0000 (09:54 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 7 Jul 2015 14:59:19 +0000 (16:59 +0200)
114 files changed:
admin/cli/fix_orphaned_question_categories.php [new file with mode: 0644]
admin/cli/install.php
admin/tool/langimport/classes/controller.php
admin/tool/langimport/index.php
admin/tool/langimport/lang/en/tool_langimport.php
admin/tool/log/store/legacy/db/tasks.php
admin/tool/log/store/legacy/version.php
admin/tool/log/store/standard/db/tasks.php
admin/tool/log/store/standard/version.php
auth/ldap/auth.php
auth/ldap/tests/plugin_test.php
auth/ldap/upgrade.txt [new file with mode: 0644]
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
course/lib.php
course/tests/courselib_test.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/locallib.php
enrol/users_forms.php
filter/emoticon/filter.php
filter/emoticon/tests/filter_test.php
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js
filter/glossary/yui/src/autolinker/js/autolinker.js
filter/upgrade.txt
filter/urltolink/filter.php
grade/edit/tree/category.php
grade/export/lib.php
grade/export/txt/tests/behat/export.feature
grade/grading/form/rubric/js/rubriceditor.js
grade/grading/form/rubric/lang/en/gradingform_rubric.php
grade/grading/form/rubric/renderer.php
grade/grading/form/rubric/styles.css
grade/grading/form/rubric/version.php
grade/lib.php
grade/report/grader/lib.php
grade/report/lib.php
grade/report/singleview/classes/local/screen/user.php
grade/tests/behat/behat_grade.php
grade/tests/behat/grade_calculated_grade_items.feature [new file with mode: 0644]
grade/tests/behat/grade_calculated_grade_items_20150627.feature [new file with mode: 0644]
grade/tests/behat/grade_minmax.feature
grade/tests/behat/grade_natural_exclude_empty.feature [new file with mode: 0644]
grade/tests/behat/grade_natural_exclude_empty_20150619.feature [new file with mode: 0644]
grade/tests/behat/grade_natural_normalisation.feature
grade/tests/behat/grade_natural_normalisation_20150619.feature [new file with mode: 0644]
group/externallib.php
group/tests/externallib_test.php
install/lang/es/install.php
install/lang/mk/install.php
lang/en/enrol.php
lang/en/grades.php
lang/en/portfolio.php
lib/adminlib.php
lib/behat/classes/behat_command.php
lib/behat/lib.php
lib/classes/message/message.php
lib/clilib.php
lib/db/install.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/filelib.php
lib/filterlib.php
lib/grade/grade_category.php
lib/grade/grade_grade.php
lib/grade/grade_item.php
lib/moodlelib.php
lib/myprofilelib.php
lib/navigationlib.php
lib/questionlib.php
lib/testing/lib.php
lib/tests/behat/behat_forms.php
lib/tests/filelib_test.php
lib/tests/filter_manager_test.php [new file with mode: 0644]
lib/tests/moodlelib_test.php
lib/tests/questionlib_test.php
lib/tests/upgradelib_test.php
lib/tests/weblib_format_text_test.php [new file with mode: 0644]
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js [new file with mode: 0644]
lib/yui/src/languninstallconfirm/build.json [new file with mode: 0644]
lib/yui/src/languninstallconfirm/js/languninstallconfirm.js [new file with mode: 0644]
lib/yui/src/languninstallconfirm/meta/languninstallconfirm.json [new file with mode: 0644]
login/signup_form.php
mod/data/classes/external.php
mod/forum/post.php
mod/glossary/showentry_ajax.php
mod/quiz/lang/en/quiz.php
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.js
mod/quiz/yui/src/toolboxes/js/resource.js
mod/scorm/player.php
mod/scorm/prereqs.php
mod/scorm/styles.css
mod/workshop/mod_form.php
repository/wikimedia/wikimedia.php
repository/youtube/lang/en/repository_youtube.php
theme/bootstrapbase/less/moodle/bootstrapoverride.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/style/moodle.css
user/lib.php
user/portfolio.php
user/view.php
version.php

diff --git a/admin/cli/fix_orphaned_question_categories.php b/admin/cli/fix_orphaned_question_categories.php
new file mode 100644 (file)
index 0000000..eeac16f
--- /dev/null
@@ -0,0 +1,103 @@
+<?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 script fixes orphaned question categories.
+ *
+ * Orphaned question categories have had their associated context deleted
+ * but the category itself remains in the database with an invalid context.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2013 Tyler Bannister (tyler.bannister@remote-learner.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__.'/../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+require_once($CFG->libdir.'/questionlib.php');
+
+$long = array('fix'  => false, 'help' => false);
+$short = array('f' => 'fix', 'h' => 'help');
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params($long, $short);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help']) {
+    $help =
+        "Fix orphaned question categories.
+
+        This scripts detects question categories that have had their
+        context deleted, thus severing them from their original purpose.
+
+        This script will find the orphaned categories and delete the unused
+        questions in each category found.  Used questions will not be
+        deleted, instead they will be moved to a rescue question category.
+
+        Options:
+        -h, --help            Print out this help
+        -f, --fix             Fix the orphaned question categories in the DB.
+                              If not specified only check and report problems to STDERR.
+        Example:
+        \$sudo -u www-data /usr/bin/php admin/cli/fix_orphaned_question_categories.php
+        \$sudo -u www-data /usr/bin/php admin/cli/fix_orphaned_question_categories.php -f
+        ";
+
+    echo $help;
+    die;
+}
+
+cli_heading('Checking for orphaned categories');
+
+
+$sql = 'SELECT qc.id, qc.contextid, qc.name
+          FROM {question_categories} qc
+     LEFT JOIN {context} c ON qc.contextid = c.id
+         WHERE c.id IS NULL';
+$categories = $DB->get_recordset_sql($sql);
+
+$i = 0;
+foreach ($categories as $category) {
+    $i += 1;
+    echo "Found orphaned category: {$category->name}\n";
+    if (!empty($options['fix'])) {
+        echo "Cleaning...";
+        // One transaction per category.
+        $transaction = $DB->start_delegated_transaction();
+        question_category_delete_safe($category);
+        $transaction->allow_commit();
+        echo "  Done!\n";
+    }
+}
+
+if (($i > 0) && !empty($options['fix'])) {
+    echo "Found and removed {$i} orphaned question categories\n";
+} else if ($i > 0) {
+    echo "Found {$i} orphaned question categories. To fix, run:\n";
+    echo "\$sudo -u www-data /usr/bin/php admin/cli/fix_orphaned_question_categories.php --fix\n";
+} else {
+    echo "No orphaned question categories found.\n";
+}
+
+
+$categories->close();
index acd6060..e43430e 100644 (file)
@@ -286,6 +286,8 @@ if ($options['help']) {
 }
 
 //Print header
+cli_logo();
+echo PHP_EOL;
 echo get_string('cliinstallheader', 'install', $CFG->target_release)."\n";
 
 //Fist select language
index d3eee58..bf43258 100644 (file)
@@ -128,7 +128,7 @@ class controller {
             event\langpack_removed::event_with_langcode($lang)->trigger();
             return true;
         } else {    // Nothing deleted, possibly due to permission error.
-            $this->errors[] = 'An error has occurred, language pack is not completely uninstalled, please check file permissions';
+            $this->errors[] = get_string('langpacknotremoved', 'tool_langimport', $lang);
             return false;
         }
     }
index b49dbd3..c1ee2fb 100644 (file)
@@ -37,11 +37,11 @@ if (empty($CFG->langotherroot)) {
     throw new moodle_exception('missingcfglangotherroot', 'tool_langimport');
 }
 
-$mode          = optional_param('mode', 0, PARAM_INT);              // action
-$pack          = optional_param_array('pack', array(), PARAM_SAFEDIR);    // pack to install
-$uninstalllang = optional_param('uninstalllang', '', PARAM_LANG);   // installed pack to uninstall
-$confirm       = optional_param('confirm', 0, PARAM_BOOL);          // uninstallation confirmation
-$purgecaches   = optional_param('purgecaches', false, PARAM_BOOL);  // explicit caches reset
+$mode               = optional_param('mode', 0, PARAM_INT);              // action
+$pack               = optional_param_array('pack', array(), PARAM_SAFEDIR);    // pack to install
+$uninstalllang      = optional_param_array('uninstalllang', array(), PARAM_LANG);// installed pack to uninstall
+$confirmtounistall  = optional_param('confirmtouninstall', '', PARAM_ALPHAEXT);  // uninstallation confirmation
+$purgecaches        = optional_param('purgecaches', false, PARAM_BOOL);  // explicit caches reset
 
 if ($purgecaches) {
     require_sesskey();
@@ -70,21 +70,30 @@ if (($mode == INSTALLATION_OF_SELECTED_LANG) and confirm_sesskey() and !empty($p
     $controller->install_languagepacks($pack);
 }
 
-if ($mode == DELETION_OF_SELECTED_LANG and !empty($uninstalllang)) {
-    if ($uninstalllang == 'en') {
+if ($mode == DELETION_OF_SELECTED_LANG and (!empty($uninstalllang) or !empty($confirmtounistall))) {
+    // Actually deleting languages, languages to delete are passed as GET parameter as string
+    // ...need to populate them to array.
+    if (empty($uninstalllang)) {
+        $uninstalllang = explode('-', $confirmtounistall);
+    }
+
+    if (in_array('en', $uninstalllang)) {
         // TODO.
-        $controller->errors[] = 'English language pack can not be uninstalled';
+        $controller->errors[] = get_string('noenglishuninstall', 'tool_langimport');
 
-    } else if (!$confirm and confirm_sesskey()) {
+    } else if (empty($confirmtounistall) and confirm_sesskey()) { // User chose langs to be deleted, show confirmation.
         echo $OUTPUT->header();
-        echo $OUTPUT->confirm(get_string('uninstallconfirm', 'tool_langimport', $uninstalllang),
-                     'index.php?mode='.DELETION_OF_SELECTED_LANG.'&uninstalllang='.$uninstalllang.'&confirm=1',
+        echo $OUTPUT->confirm(get_string('uninstallconfirm', 'tool_langimport', implode(', ', $uninstalllang)),
+                     'index.php?mode='.DELETION_OF_SELECTED_LANG.'&confirmtouninstall='.implode('-', $uninstalllang),
                      'index.php');
         echo $OUTPUT->footer();
         die;
 
-    } else if (confirm_sesskey()) {
-        $controller->uninstall_language($uninstalllang);
+    } else if (confirm_sesskey()) {   // Deleting languages.
+        foreach ($uninstalllang as $ulang) {
+            $controller->uninstall_language($ulang);
+        }
+
     }
 }
 
@@ -158,10 +167,13 @@ echo html_writer::start_tag('form', array('id' => 'uninstallform', 'action' => $
 echo html_writer::start_tag('fieldset');
 echo html_writer::label(get_string('installedlangs', 'tool_langimport'), 'menuuninstalllang');
 echo html_writer::empty_tag('br');
-echo html_writer::select($installedlangs, 'uninstalllang', '', false, array('size' => 15));
+echo html_writer::select($installedlangs, 'uninstalllang[]', '', false, array('size' => 15, 'multiple' => 'multiple'));
 echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
 echo html_writer::empty_tag('br');
-echo html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('uninstall', 'tool_langimport')));
+echo html_writer::empty_tag('input', array('id' => 'languninstallbutton',
+                                         'type' => 'submit',
+                                         'value' => get_string('uninstall', 'tool_langimport'))
+                            );
 echo html_writer::end_tag('fieldset');
 echo html_writer::end_tag('form');
 if ($remote) {
@@ -198,5 +210,13 @@ if (!empty($options)) {
 echo html_writer::end_tag('tr');
 echo html_writer::end_tag('table');
 echo $OUTPUT->box_end();
+
+$uninstallurl = new moodle_url('/admin/tool/langimport/index.php');
+$PAGE->requires->strings_for_js(array('uninstallconfirm', 'uninstall', 'selectlangs', 'noenglishuninstall'),
+                                'tool_langimport');
+$PAGE->requires->yui_module('moodle-core-languninstallconfirm',
+                            'Y.M.core.languninstallconfirm.init',
+                             array(array('uninstallUrl' => $uninstallurl->out()))
+                            );
 echo $OUTPUT->footer();
 die();
index fba12f9..65a752d 100644 (file)
@@ -30,6 +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['langpackremovedevent'] = 'Language pack uninstalled';
 $string['langpackupdateskipped'] = 'Update of \'{$a}\' language pack skipped';
 $string['langpackuptodate'] = 'Language pack \'{$a}\' is up-to-date';
@@ -38,11 +39,12 @@ $string['langpackupdatedevent'] = 'Language pack updated';
 $string['langupdatecomplete'] = 'Language pack update completed';
 $string['missingcfglangotherroot'] = 'Missing configuration value $CFG->langotherroot';
 $string['missinglangparent'] = 'Missing parent language <em>{$a->parent}</em> of <em>{$a->lang}</em>.';
+$string['noenglishuninstall'] = 'English language pack can not be uninstalled';
 $string['nolangupdateneeded'] = 'All your language packs are up to date, no update is needed';
 $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['uninstall'] = 'Uninstall selected language pack';
-$string['uninstallconfirm'] = 'You are about to completely uninstall language pack {$a}, are you sure?';
+$string['selectlangs'] = 'Select languages to unistall!';
+$string['uninstall'] = 'Uninstall selected language packs';
+$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 24522d9..7aeed67 100644 (file)
@@ -28,7 +28,7 @@ $tasks = array(
     array(
         'classname' => '\logstore_legacy\task\cleanup_task',
         'blocking' => 0,
-        'minute' => '*',
+        'minute' => 'R',
         'hour' => '5',
         'day' => '*',
         'dayofweek' => '*',
index 0cf0cdd..e1917a7 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version = 2015051100; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2015070700; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires = 2015050500; // Requires this Moodle version.
 $plugin->component = 'logstore_legacy'; // Full name of the plugin (used for diagnostics).
index f51c812..fa6cc9d 100644 (file)
@@ -28,7 +28,7 @@ $tasks = array(
     array(
         'classname' => '\logstore_standard\task\cleanup_task',
         'blocking' => 0,
-        'minute' => '*',
+        'minute' => 'R',
         'hour' => '4',
         'day' => '*',
         'dayofweek' => '*',
index b164b2a..3d6cb31 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version = 2015051100; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2015070700; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires = 2015050500; // Requires this Moodle version.
 $plugin->component = 'logstore_standard'; // Full name of the plugin (used for diagnostics).
index 5e20dbf..6090ebd 100644 (file)
@@ -534,6 +534,7 @@ class auth_plugin_ldap extends auth_plugin_base {
      *
      * @param object $user new user object
      * @param boolean $notify print notice with link and terminate
+     * @return boolean success
      */
     function user_signup($user, $notify=true) {
         global $CFG, $DB, $PAGE, $OUTPUT;
@@ -889,7 +890,7 @@ class auth_plugin_ldap extends auth_plugin_base {
 
                 foreach ($users as $user) {
                     echo "\t"; print_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id));
-                    if (!$this->update_user_record($user->username, $updatekeys)) {
+                    if (!$this->update_user_record($user->username, $updatekeys, true)) {
                         echo ' - '.get_string('skipped');
                     }
                     echo "\n";
@@ -987,8 +988,11 @@ class auth_plugin_ldap extends auth_plugin_base {
      *
      * @param string $username username
      * @param boolean $updatekeys true to update the local record with the external LDAP values.
+     * @param bool $triggerevent set false if user_updated event should not be triggered.
+     *             This will not affect user_password_updated event triggering.
+     * @return stdClass|bool updated user record or false if there is no new info to update.
      */
-    function update_user_record($username, $updatekeys = false) {
+    function update_user_record($username, $updatekeys = false, $triggerevent = false) {
         global $CFG, $DB;
 
         // Just in case check text case
@@ -1030,7 +1034,7 @@ class auth_plugin_ldap extends auth_plugin_base {
                         }
                     }
                 }
-                user_update_user($newuser, false, false);
+                user_update_user($newuser, false, $triggerevent);
             }
         } else {
             return false;
index 59a1f9d..ba82adc 100644 (file)
@@ -136,9 +136,25 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         $auth = get_auth_plugin('ldap');
 
         ob_start();
+        $sink = $this->redirectEvents();
         $auth->sync_users(true);
+        $events = $sink->get_events();
+        $sink->close();
         ob_end_clean();
 
+        // Check events, 5 users created with 2 users having roles.
+        $this->assertCount(7, $events);
+        foreach ($events as $index => $event) {
+            $usercreatedindex = array(0, 2, 4, 5, 6);
+            $roleassignedindex = array (1, 3);
+            if (in_array($index, $usercreatedindex)) {
+                $this->assertInstanceOf('\core\event\user_created', $event);
+            }
+            if (in_array($index, $roleassignedindex)) {
+                $this->assertInstanceOf('\core\event\role_assigned', $event);
+            }
+        }
+
         $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
         $this->assertEquals(2, $DB->count_records('role_assignments'));
         $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
@@ -150,9 +166,15 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         $this->delete_ldap_user($connection, $topdn, 1);
 
         ob_start();
+        $sink = $this->redirectEvents();
         $auth->sync_users(true);
+        $events = $sink->get_events();
+        $sink->close();
         ob_end_clean();
 
+        // Check events, no new event.
+        $this->assertCount(0, $events);
+
         $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
         $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
         $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
@@ -166,9 +188,17 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         $auth = get_auth_plugin('ldap');
 
         ob_start();
+        $sink = $this->redirectEvents();
         $auth->sync_users(true);
+        $events = $sink->get_events();
+        $sink->close();
         ob_end_clean();
 
+        // Check events, 1 user got updated.
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertInstanceOf('\core\event\user_updated', $event);
+
         $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
         $this->assertEquals(0, $DB->count_records('user', array('auth'=>'nologin', 'username'=>'username1')));
         $this->assertEquals(1, $DB->count_records('user', array('auth'=>'ldap', 'suspended'=>'1', 'username'=>'username1')));
@@ -179,9 +209,17 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         $this->create_ldap_user($connection, $topdn, 1);
 
         ob_start();
+        $sink = $this->redirectEvents();
         $auth->sync_users(true);
+        $events = $sink->get_events();
+        $sink->close();
         ob_end_clean();
 
+        // Check events, 1 user got updated.
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertInstanceOf('\core\event\user_updated', $event);
+
         $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
         $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
         $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
@@ -191,9 +229,17 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         $DB->set_field('user', 'auth', 'nologin', array('username'=>'username1'));
 
         ob_start();
+        $sink = $this->redirectEvents();
         $auth->sync_users(true);
+        $events = $sink->get_events();
+        $sink->close();
         ob_end_clean();
 
+        // Check events, 1 user got updated.
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertInstanceOf('\core\event\user_updated', $event);
+
         $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
         $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
         $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
@@ -208,9 +254,19 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         $this->delete_ldap_user($connection, $topdn, 1);
 
         ob_start();
+        $sink = $this->redirectEvents();
         $auth->sync_users(true);
+        $events = $sink->get_events();
+        $sink->close();
         ob_end_clean();
 
+        // Check events, 2 events role_unassigned and user_deleted.
+        $this->assertCount(2, $events);
+        $event = array_pop($events);
+        $this->assertInstanceOf('\core\event\user_deleted', $event);
+        $event = array_pop($events);
+        $this->assertInstanceOf('\core\event\role_unassigned', $event);
+
         $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
         $this->assertEquals(0, $DB->count_records('user', array('username'=>'username1')));
         $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
@@ -221,9 +277,19 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         $this->create_ldap_user($connection, $topdn, 1);
 
         ob_start();
+        $sink = $this->redirectEvents();
         $auth->sync_users(true);
+        $events = $sink->get_events();
+        $sink->close();
         ob_end_clean();
 
+        // Check events, 2 events role_assigned and user_created.
+        $this->assertCount(2, $events);
+        $event = array_pop($events);
+        $this->assertInstanceOf('\core\event\role_assigned', $event);
+        $event = array_pop($events);
+        $this->assertInstanceOf('\core\event\user_created', $event);
+
         $this->assertEquals(6, $DB->count_records('user', array('auth'=>'ldap')));
         $this->assertEquals(1, $DB->count_records('user', array('username'=>'username1')));
         $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
diff --git a/auth/ldap/upgrade.txt b/auth/ldap/upgrade.txt
new file mode 100644 (file)
index 0000000..8ac9073
--- /dev/null
@@ -0,0 +1,4 @@
+This files describes API changes in the auth_ldap code.
+=== 2.9.1 ===
+* auth_plugin_ldap::update_user_record() accepts an additional (optional) param
+  to trigger update event.
index f24abce..89c7534 100644 (file)
@@ -939,6 +939,7 @@ class backup_gradebook_structure_step extends backup_structure_step {
     }
 
     protected function define_structure() {
+        global $CFG, $DB;
 
         // are we including user info?
         $userinfo = $this->get_setting_value('users');
@@ -999,6 +1000,13 @@ class backup_gradebook_structure_step extends backup_structure_step {
         $gradebook->add_child($grade_settings);
         $grade_settings->add_child($grade_setting);
 
+        // Add attribute with gradebook calculation freeze date if needed.
+        $gradebookcalculationfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
+        if ($gradebookcalculationfreeze) {
+            $gradebook->add_attributes(array('calculations_freeze'));
+            $gradebook->get_attribute('calculations_freeze')->set_value($gradebookcalculationfreeze);
+        }
+
         // Define sources
 
         //Include manual, category and the course grade item
@@ -1023,7 +1031,18 @@ class backup_gradebook_structure_step extends backup_structure_step {
 
         $letter->set_source_table('grade_letters', array('contextid' => backup::VAR_CONTEXTID));
 
-        $grade_setting->set_source_table('grade_settings', array('courseid' => backup::VAR_COURSEID));
+        // Set the grade settings source, forcing the inclusion of minmaxtouse if not present.
+        $settings = array();
+        $rs = $DB->get_recordset('grade_settings', array('courseid' => $this->get_courseid()));
+        foreach ($rs as $record) {
+            $settings[$record->name] = $record;
+        }
+        $rs->close();
+        if (!isset($settings['minmaxtouse'])) {
+            $settings['minmaxtouse'] = (object) array('name' => 'minmaxtouse', 'value' => $CFG->grade_minmaxtouse);
+        }
+        $grade_setting->set_source_array($settings);
+
 
         // Annotations (both as final as far as they are going to be exported in next steps)
         $grade_item->annotate_ids('scalefinal', 'scaleid'); // Straight as scalefinal because it's > 0
index c9038cf..769e7ff 100644 (file)
@@ -142,6 +142,18 @@ class restore_gradebook_structure_step extends restore_structure_step {
     }
 
     protected function process_gradebook($data) {
+        // For non-merge restore types:
+        // Unset 'gradebook_calculations_freeze_' in the course and replace with the one from the backup.
+        $target = $this->get_task()->get_target();
+        if ($target == backup::TARGET_CURRENT_DELETING || $target == backup::TARGET_EXISTING_DELETING) {
+            set_config('gradebook_calculations_freeze_' . $this->get_courseid(), null);
+        }
+        if (!empty($data['calculations_freeze'])) {
+            if ($target == backup::TARGET_NEW_COURSE || $target == backup::TARGET_CURRENT_DELETING ||
+                    $target == backup::TARGET_EXISTING_DELETING) {
+                set_config('gradebook_calculations_freeze_' . $this->get_courseid(), $data['calculations_freeze']);
+            }
+        }
     }
 
     protected function process_grade_item($data) {
@@ -323,13 +335,23 @@ class restore_gradebook_structure_step extends restore_structure_step {
 
         $data->courseid = $this->get_courseid();
 
+        $target = $this->get_task()->get_target();
+        if ($data->name == 'minmaxtouse' &&
+                ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING)) {
+            // We never restore minmaxtouse during merge.
+            return;
+        }
+
         if (!$DB->record_exists('grade_settings', array('courseid' => $data->courseid, 'name' => $data->name))) {
             $newitemid = $DB->insert_record('grade_settings', $data);
         } else {
             $newitemid = $data->id;
         }
 
-        $this->set_mapping('grade_setting', $oldid, $newitemid);
+        if (!empty($oldid)) {
+            // In rare cases (minmaxtouse), it is possible that there wasn't any ID associated with the setting.
+            $this->set_mapping('grade_setting', $oldid, $newitemid);
+        }
     }
 
     /**
@@ -451,9 +473,104 @@ class restore_gradebook_structure_step extends restore_structure_step {
         }
         $rs->close();
 
+        // Check what to do with the minmaxtouse setting.
+        $this->check_minmaxtouse();
+
+        // Freeze gradebook calculations if needed.
+        $this->gradebook_calculation_freeze();
+
         // Restore marks items as needing update. Update everything now.
         grade_regrade_final_grades($this->get_courseid());
     }
+
+    /**
+     * Freeze gradebook calculation if needed.
+     *
+     * This is similar to various upgrade scripts that check if the freeze is needed.
+     */
+    protected function gradebook_calculation_freeze() {
+        global $CFG;
+        $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
+        preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
+        $backupbuild = (int)$matches[1];
+
+        // Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
+        if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150619) {
+            require_once($CFG->libdir . '/db/upgradelib.php');
+            upgrade_extra_credit_weightoverride($this->get_courseid());
+        }
+        // Calculated grade items need recalculating for backups made between 2.8 release (20141110) and the fix release (20150627).
+        if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150627) {
+            require_once($CFG->libdir . '/db/upgradelib.php');
+            upgrade_calculated_grade_items($this->get_courseid());
+        }
+    }
+
+    /**
+     * Checks what should happen with the course grade setting minmaxtouse.
+     *
+     * This is related to the upgrade step at the time the setting was added.
+     *
+     * @see MDL-48618
+     * @return void
+     */
+    protected function check_minmaxtouse() {
+        global $CFG, $DB;
+        require_once($CFG->libdir . '/gradelib.php');
+
+        $userinfo = $this->task->get_setting_value('users');
+        $settingname = 'minmaxtouse';
+        $courseid = $this->get_courseid();
+        $minmaxtouse = $DB->get_field('grade_settings', 'value', array('courseid' => $courseid, 'name' => $settingname));
+        $version28start = 2014111000.00;
+        $version28last = 2014111006.05;
+        $version29start = 2015051100.00;
+        $version29last = 2015060400.02;
+
+        $target = $this->get_task()->get_target();
+        if ($minmaxtouse === false &&
+                ($target != backup::TARGET_CURRENT_ADDING && $target != backup::TARGET_EXISTING_ADDING)) {
+            // The setting was not found because this setting did not exist at the time the backup was made.
+            // And we are not restoring as merge, in which case we leave the course as it was.
+            $version = $this->get_task()->get_info()->moodle_version;
+
+            if ($version < $version28start) {
+                // We need to set it to use grade_item, but only if the site-wide setting is different. No need to notice them.
+                if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_ITEM) {
+                    grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_ITEM);
+                }
+
+            } else if (($version >= $version28start && $version < $version28last) ||
+                    ($version >= $version29start && $version < $version29last)) {
+                // They should be using grade_grade when the course has inconsistencies.
+
+                $sql = "SELECT gi.id
+                          FROM {grade_items} gi
+                          JOIN {grade_grades} gg
+                            ON gg.itemid = gi.id
+                         WHERE gi.courseid = ?
+                           AND (gi.itemtype != ? AND gi.itemtype != ?)
+                           AND (gg.rawgrademax != gi.grademax OR gg.rawgrademin != gi.grademin)";
+
+                // The course can only have inconsistencies when we restore the user info,
+                // we do not need to act on existing grades that were not restored as part of this backup.
+                if ($userinfo && $DB->record_exists_sql($sql, array($courseid, 'course', 'category'))) {
+
+                    // Display the notice as we do during upgrade.
+                    set_config('show_min_max_grades_changed_' . $courseid, 1);
+
+                    if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_GRADE) {
+                        // We need set the setting as their site-wise setting is not GRADE_MIN_MAX_FROM_GRADE_GRADE.
+                        // If they are using the site-wide grade_grade setting, we only want to notice them.
+                        grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_GRADE);
+                    }
+                }
+
+            } else {
+                // This should never happen because from now on minmaxtouse is always saved in backups.
+            }
+        }
+    }
 }
 
 /**
index e49bdf1..7c89127 100644 (file)
@@ -1633,7 +1633,7 @@ function set_coursemodule_visible($id, $visible) {
 }
 
 /**
- * This function will handles the whole deletion process of a module. This includes calling
+ * This function will handle the whole deletion process of a module. This includes calling
  * the modules delete_instance function, deleting files, events, grades, conditional data,
  * the data in the course_module and course_sections table and adding a module deletion
  * event to the DB.
@@ -1645,9 +1645,10 @@ function course_delete_module($cmid) {
     global $CFG, $DB;
 
     require_once($CFG->libdir.'/gradelib.php');
+    require_once($CFG->libdir.'/questionlib.php');
     require_once($CFG->dirroot.'/blog/lib.php');
     require_once($CFG->dirroot.'/calendar/lib.php');
-    require_once($CFG->dirroot . '/tag/lib.php');
+    require_once($CFG->dirroot.'/tag/lib.php');
 
     // Get the course module.
     if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
@@ -1679,6 +1680,9 @@ function course_delete_module($cmid) {
             "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
     }
 
+    // Delete activity context questions and question categories.
+    question_delete_activity($cm);
+
     // Call the delete_instance function, if it returns false throw an exception.
     if (!$deleteinstancefunction($cm->instance)) {
         throw new moodle_exception('cannotdeletemoduleinstance', '', '', null,
index a40bea6..27b2603 100644 (file)
@@ -539,7 +539,23 @@ class core_course_courselib_testcase extends advanced_testcase {
         return $moduleinfo;
    }
 
+    /**
+     * Data provider for course_delete module
+     *
+     * @return array An array of arrays contain test data
+     */
+    public function provider_course_delete_module() {
+        $data = array();
 
+        $data['assign'] = array('assign', array('duedate' => time()));
+        $data['quiz'] = array('quiz', array('duedate' => time()));
+
+        return $data;
+    }
+
+    /**
+     * Test the create_course function
+     */
     public function test_create_course() {
         global $DB;
         $this->resetAfterTest(true);
@@ -1476,51 +1492,92 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEquals($pagecm->visible, 0);
     }
 
-    public function test_course_delete_module() {
+    /**
+     * Tests the function that deletes a course module
+     *
+     * @param string $type The type of module for the test
+     * @param array $options The options for the module creation
+     * @dataProvider provider_course_delete_module
+     */
+    public function test_course_delete_module($type, $options) {
         global $DB;
         $this->resetAfterTest(true);
         $this->setAdminUser();
 
         // Create course and modules.
         $course = $this->getDataGenerator()->create_course(array('numsections' => 5));
+        $options['course'] = $course->id;
 
         // Generate an assignment with due date (will generate a course event).
-        $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(), 'course' => $course->id));
+        $module = $this->getDataGenerator()->create_module($type, $options);
 
         // Get the module context.
-        $modcontext = context_module::instance($assign->cmid);
+        $modcontext = context_module::instance($module->cmid);
 
         // Verify context exists.
         $this->assertInstanceOf('context_module', $modcontext);
 
-        // Add some tags to this assignment.
-        tag_set('assign', $assign->id, array('Tag 1', 'Tag 2', 'Tag 3'), 'mod_assign', $modcontext->id);
-
-        // Confirm the tag instances were added.
-        $this->assertEquals(3, $DB->count_records('tag_instance', array('component' => 'mod_assign', 'contextid' =>
-            $modcontext->id)));
-
-        // Verify event assignment event has been generated.
-        $eventcount = $DB->count_records('event', array('instance' => $assign->id, 'modulename' => 'assign'));
-        $this->assertEquals(1, $eventcount);
+        // Make module specific messes.
+        switch ($type) {
+            case 'assign':
+                // Add some tags to this assignment.
+                tag_set('assign', $module->id, array('Tag 1', 'Tag 2', 'Tag 3'), 'mod_assign', $modcontext->id);
+
+                // Confirm the tag instances were added.
+                $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id);
+                $this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
+
+                // Verify event assignment event has been generated.
+                $eventcount = $DB->count_records('event', array('instance' => $module->id, 'modulename' => $type));
+                $this->assertEquals(1, $eventcount);
+
+                break;
+            case 'quiz':
+                $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
+                $qcat = $qgen->create_question_category(array('contextid' => $modcontext->id));
+                $questions = array(
+                    $qgen->create_question('shortanswer', null, array('category' => $qcat->id)),
+                    $qgen->create_question('shortanswer', null, array('category' => $qcat->id)),
+                );
+                $this->expectOutputRegex('/'.get_string('unusedcategorydeleted', 'question').'/');
+                break;
+            default:
+                break;
+        }
 
         // Run delete..
-        course_delete_module($assign->cmid);
+        course_delete_module($module->cmid);
 
         // Verify the context has been removed.
-        $this->assertFalse(context_module::instance($assign->cmid, IGNORE_MISSING));
+        $this->assertFalse(context_module::instance($module->cmid, IGNORE_MISSING));
 
         // Verify the course_module record has been deleted.
-        $cmcount = $DB->count_records('course_modules', array('id' => $assign->cmid));
+        $cmcount = $DB->count_records('course_modules', array('id' => $module->cmid));
         $this->assertEmpty($cmcount);
 
-        // Verify event assignment events have been removed.
-        $eventcount = $DB->count_records('event', array('instance' => $assign->id, 'modulename' => 'assign'));
-        $this->assertEmpty($eventcount);
-
-        // Verify the tag instances were deleted.
-        $this->assertEquals(0, $DB->count_records('tag_instance', array('component' => 'mod_assign', 'contextid' =>
-            $modcontext->id)));
+        // Test clean up of module specific messes.
+        switch ($type) {
+            case 'assign':
+                // Verify event assignment events have been removed.
+                $eventcount = $DB->count_records('event', array('instance' => $module->id, 'modulename' => $type));
+                $this->assertEmpty($eventcount);
+
+                // Verify the tag instances were deleted.
+                $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id);
+                $this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
+                break;
+            case 'quiz':
+                // Verify category deleted.
+                $criteria = array('contextid' => $modcontext->id);
+                $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
+
+                // Verify questions deleted.
+                $criteria = array('category' => $qcat->id);
+                $this->assertEquals(0, $DB->count_records('question', $criteria));
+                break;
+            default:
+                break;
+        }
     }
 
     /**
index 66da159..c8e086a 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 a992811..1704f91 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 66da159..c8e086a 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 11ed3f9..c8683e3 100644 (file)
@@ -665,6 +665,13 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
         Y.later(100, this, function() {
             activity.one(SELECTOR.EDITTITLE).focus();
         });
+
+        // TODO MDL-50768 This hack is to keep Behat happy until they release a version of
+        // MinkSelenium2Driver that fixes
+        // https://github.com/Behat/MinkSelenium2Driver/issues/80.
+        if (!Y.one('input[name=title]')) {
+            Y.one('body').append('<input type="text" name="title" style="display: none">');
+        }
     },
 
     /**
index 7ab3e3d..2115b15 100644 (file)
@@ -280,8 +280,13 @@ class course_enrolment_manager {
 
         // Group condition.
         if ($this->groupfilter) {
-            $sql .= " AND gm.groupid = :groupid";
-            $params['groupid'] = $this->groupfilter;
+            if ($this->groupfilter < 0) {
+                // Show users who are not in any group.
+                $sql .= " AND gm.groupid IS NULL";
+            } else {
+                $sql .= " AND gm.groupid = :groupid";
+                $params['groupid'] = $this->groupfilter;
+            }
         }
 
         // Status condition.
index b2ac13c..97b62b7 100644 (file)
@@ -167,6 +167,7 @@ class enrol_users_filter_form extends moodleform {
         // Filter by group.
         $allgroups = $manager->get_all_groups();
         $groupsmenu[0] = get_string('allparticipants');
+        $groupsmenu[-1] = get_string('nogroup', 'enrol');
         foreach($allgroups as $gid => $unused) {
             $groupsmenu[$gid] = $allgroups[$gid]->name;
         }
index b7b68b2..d579bf6 100644 (file)
@@ -32,14 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 
 class filter_emoticon extends moodle_text_filter {
 
-    /**
-     * @var array global configuration for this filter
-     *
-     * This might be eventually moved into parent class if we found it
-     * useful for other filters, too.
-     */
-    protected static $globalconfig;
-
     /**
      * Apply the filter to the text
      *
@@ -56,7 +48,7 @@ class filter_emoticon extends moodle_text_filter {
             // be stripped. therefore, we do nothing
             return $text;
         }
-        if (in_array($options['originalformat'], explode(',', $this->get_global_config('formats')))) {
+        if (in_array($options['originalformat'], explode(',', get_config('filter_emoticon', 'formats')))) {
             $this->replace_emoticons($text);
         }
         return $text;
@@ -66,40 +58,6 @@ class filter_emoticon extends moodle_text_filter {
     // internal implementation starts here
     ////////////////////////////////////////////////////////////////////////////
 
-    /**
-     * Returns the global filter setting
-     *
-     * If the $name is provided, returns single value. Otherwise returns all
-     * global settings in object. Returns null if the named setting is not
-     * found.
-     *
-     * @param mixed $name optional config variable name, defaults to null for all
-     * @return string|object|null
-     */
-    protected function get_global_config($name=null) {
-        $this->load_global_config();
-        if (is_null($name)) {
-            return self::$globalconfig;
-
-        } elseif (array_key_exists($name, self::$globalconfig)) {
-            return self::$globalconfig->{$name};
-
-        } else {
-            return null;
-        }
-    }
-
-    /**
-     * Makes sure that the global config is loaded in $this->globalconfig
-     *
-     * @return void
-     */
-    protected function load_global_config() {
-        if (is_null(self::$globalconfig)) {
-            self::$globalconfig = get_config(get_class($this));
-        }
-    }
-
     /**
      * Replace emoticons found in the text with their images
      *
index 38b69cc..8aba222 100644 (file)
@@ -69,6 +69,6 @@ class testable_filter_emoticon extends filter_emoticon {
         // Use this context for filtering.
         $this->context = context_system::instance();
         // Define FORMAT_HTML as only one filtering in DB.
-        set_config('formats', implode(',', array(FORMAT_HTML)), get_class($this));
+        set_config('formats', implode(',', array(FORMAT_HTML)), 'filter_emoticon');
     }
 }
index 66d43cd..b932ef7 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js differ
index 7995106..34d316d 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js differ
index 66d43cd..b932ef7 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js differ
index a6c6986..564f348 100644 (file)
@@ -17,6 +17,7 @@ AUTOLINKER = function() {
 };
 Y.extend(AUTOLINKER, Y.Base, {
     overlay : null,
+    alertpanels: {},
     initializer : function() {
         var self = this;
         Y.delegate('click', function(e){
@@ -62,7 +63,10 @@ Y.extend(AUTOLINKER, Y.Base, {
         var data,
             key,
             alertpanel,
-            definition;
+            alertpanelid,
+            definition,
+            position;
+        var self = this;
         try {
             data = Y.JSON.parse(content);
             if (data.success){
@@ -74,6 +78,18 @@ Y.extend(AUTOLINKER, Y.Base, {
                         message:definition, modal:false, yesLabel: M.util.get_string('ok', 'moodle')});
                     Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(alertpanel.get('boundingBox')))});
                     Y.Node.one('#id_yuialertconfirm-' + alertpanel.get('COUNT')).focus();
+
+                    // Register alertpanel for stacking.
+                    alertpanelid = '#moodle-dialogue-' + alertpanel.get('COUNT');
+                    alertpanel.on('complete', this._deletealertpanel(self.alertpanels, alertpanelid));
+
+                    // We already have some windows opened, so set the right position...
+                    if (!Y.Object.isEmpty(this.alertpanels)){
+                        position = this._getLatestWindowPosition();
+                        Y.Node.one(alertpanelid).setXY([position[0] + 10, position[1] + 10]);
+                    }
+
+                    this.alertpanels[alertpanelid] = Y.Node.one(alertpanelid).getXY();
                 }
 
                 return true;
@@ -84,6 +100,18 @@ Y.extend(AUTOLINKER, Y.Base, {
             new M.core.exception(e);
         }
         return false;
+    },
+    _getLatestWindowPosition : function() {
+        var lastPosition = [0, 0];
+        Y.Object.each(this.alertpanels, function(position) {
+            if (position[0] > lastPosition[0]){
+                lastPosition = position;
+            }
+        });
+        return lastPosition;
+    },
+    _deletealertpanel : function(alertpanels, alertpanelid) {
+        delete alertpanels[alertpanelid];
     }
 }, {
     NAME : AUTOLINKERNAME,
index d7bfb3d..ddfbd82 100644 (file)
@@ -1,6 +1,18 @@
 This file describes API changes in core filter API and plugins,
 information provided here is intended especially for developers.
 
+=== 3.0 ===
+
+* New argument $skipfilters to filter_manager::filter_text to allow applying
+  the filters with a given one omitted.
+
+* New admin setting class admin_setting_filter_types which can be used if you
+  want to make the disablefilters value in your code configurable.
+
+* Methods filter_manager::text_filtering_hash and moodle_text_filter::hash have been
+  deprecated. There were use by the old Moodle filtered text caching system
+  that was removed several releases ago.
+
 === 2.7 ===
 
 * Finally filter may use $PAGE and $OUTPUT, yay!
index 354dbe1..b232eda 100644 (file)
@@ -51,7 +51,7 @@ class filter_urltolink extends moodle_text_filter {
             // be stripped. therefore, we do nothing
             return $text;
         }
-        if (in_array($options['originalformat'], explode(',', $this->get_global_config('formats')))) {
+        if (in_array($options['originalformat'], explode(',', get_config('filter_urltolink', 'formats')))) {
             $this->convert_urls_into_links($text);
         }
         return $text;
@@ -61,40 +61,6 @@ class filter_urltolink extends moodle_text_filter {
     // internal implementation starts here
     ////////////////////////////////////////////////////////////////////////////
 
-    /**
-     * Returns the global filter setting
-     *
-     * If the $name is provided, returns single value. Otherwise returns all
-     * global settings in object. Returns null if the named setting is not
-     * found.
-     *
-     * @param mixed $name optional config variable name, defaults to null for all
-     * @return string|object|null
-     */
-    protected function get_global_config($name=null) {
-        $this->load_global_config();
-        if (is_null($name)) {
-            return self::$globalconfig;
-
-        } elseif (array_key_exists($name, self::$globalconfig)) {
-            return self::$globalconfig->{$name};
-
-        } else {
-            return null;
-        }
-    }
-
-    /**
-     * Makes sure that the global config is loaded in $this->globalconfig
-     *
-     * @return void
-     */
-    protected function load_global_config() {
-        if (is_null(self::$globalconfig)) {
-            self::$globalconfig = get_config('filter_urltolink');
-        }
-    }
-
     /**
      * Given some text this function converts any URLs it finds into HTML links
      *
@@ -182,7 +148,7 @@ class filter_urltolink extends moodle_text_filter {
             $text = str_replace(array_keys($ignoretags),$ignoretags,$text);
         }
 
-        if ($this->get_global_config('embedimages')) {
+        if (get_config('filter_urltolink', 'embedimages')) {
             // now try to inject the images, this code was originally in the mediapluing filter
             // this may be useful only if somebody relies on the fact the links in FORMAT_MOODLE get converted
             // to URLs which in turn change to real images
index 206b12d..9695c9c 100644 (file)
@@ -86,15 +86,31 @@ if ($id) {
     } else {
         $category->grade_item_aggregationcoef = format_float($category->grade_item_aggregationcoef, 4);
     }
-
-    if ($category->aggregation == GRADE_AGGREGATE_SUM) {
-        // Input fields for grademin and grademax are disabled for the "Natural" category,
-        // this means they will be ignored if user does not change aggregation method.
-        // But if user does change aggregation method the default values should be used.
-        $category->grademax = 100;
-        $category->grade_item_grademax = 100;
-        $category->grademin = 0;
-        $category->grade_item_grademin = 0;
+    // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
+    // wish to update the grades.
+    $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $courseid);
+    // Stick with the original code if the grade book is frozen.
+    if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
+        if ($category->aggregation == GRADE_AGGREGATE_SUM) {
+            // Input fields for grademin and grademax are disabled for the "Natural" category,
+            // this means they will be ignored if user does not change aggregation method.
+            // But if user does change aggregation method the default values should be used.
+            $category->grademax = 100;
+            $category->grade_item_grademax = 100;
+            $category->grademin = 0;
+            $category->grade_item_grademin = 0;
+        }
+    } else {
+        if ($category->aggregation == GRADE_AGGREGATE_SUM && !$grade_item->is_calculated()) {
+            // Input fields for grademin and grademax are disabled for the "Natural" category,
+            // this means they will be ignored if user does not change aggregation method.
+            // But if user does change aggregation method the default values should be used.
+            // This does not apply to calculated category totals.
+            $category->grademax = 100;
+            $category->grade_item_grademax = 100;
+            $category->grademin = 0;
+            $category->grade_item_grademin = 0;
+        }
     }
 
 } else {
index 7dd9a84..e817943 100644 (file)
@@ -237,7 +237,24 @@ abstract class grade_export {
         if (is_array($this->displaytype) && !is_null($gradedisplayconst)) {
             $displaytype = $gradedisplayconst;
         }
-        return grade_format_gradevalue($grade->finalgrade, $this->grade_items[$grade->itemid], false, $displaytype, $this->decimalpoints);
+
+        $gradeitem = $this->grade_items[$grade->itemid];
+
+        // We are going to store the min and max so that we can "reset" the grade_item for later.
+        $grademax = $gradeitem->grademax;
+        $grademin = $gradeitem->grademin;
+
+        // Updating grade_item with this grade_grades min and max.
+        $gradeitem->grademax = $grade->get_grade_max();
+        $gradeitem->grademin = $grade->get_grade_min();
+
+        $formattedgrade = grade_format_gradevalue($grade->finalgrade, $gradeitem, false, $displaytype, $this->decimalpoints);
+
+        // Resetting the grade item in case it is reused.
+        $gradeitem->grademax = $grademax;
+        $gradeitem->grademin = $grademin;
+
+        return $formattedgrade;
     }
 
     /**
index 99fc29c..5670d2c 100644 (file)
@@ -19,6 +19,7 @@ Feature: I need to export grades as text
     And the following "activities" exist:
       | activity | course | idnumber | name | intro | assignsubmission_onlinetext_enabled |
       | assign | C1 | a1 | Test assignment name | Submit your online text | 1 |
+      | assign | C1 | a2 | Test assignment name 2 | Submit your online text | 1 |
     And I log in as "teacher1"
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
@@ -59,6 +60,8 @@ Feature: I need to export grades as text
     Then I should see "Student,1"
     And I should see "80.00 %"
     And I should see "B-"
+    And I should not see "40.00 %"
+    And I should not see ",F,"
 
   @javascript
   Scenario: Export grades as text using real, percentages and letters
@@ -71,4 +74,6 @@ Feature: I need to export grades as text
     Then I should see "Student,1"
     And I should see "80.00"
     And I should see "80.00 %"
-    And I should see "B-"
\ No newline at end of file
+    And I should see "B-"
+    And I should not see "40.00 %"
+    And I should not see ",F,"
\ No newline at end of file
index 6b9137a..5304b82 100644 (file)
@@ -143,7 +143,7 @@ M.gradingform_rubriceditor.buttonclick = function(e, confirmed) {
         elements_str = '#rubric-'+name+' .criterion'
     }
     // prepare the id of the next inserted level or criterion
-    if (action == 'addcriterion' || action == 'addlevel') {
+    if (action == 'addcriterion' || action == 'addlevel' || action == 'duplicate' ) {
         var newid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .criterion')
         var newlevid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .level')
     }
@@ -204,6 +204,41 @@ M.gradingform_rubriceditor.buttonclick = function(e, confirmed) {
             dialog_options['message'] = M.util.get_string('confirmdeletecriterion', 'gradingform_rubric')
             M.util.show_confirm_dialog(e, dialog_options);
         }
+    } else if (chunks.length == 4 && action == 'duplicate') {
+        // Duplicate criterion.
+        var levelsdef = [], levelsscores = [0], levidx = null;
+        var parentel = Y.one('#'+name+'-criteria');
+        if (parentel.one('>tbody')) { parentel = parentel.one('>tbody'); }
+
+        var source = Y.one('#'+name+'-criteria-'+chunks[2]);
+        if (source.all('.level')) {
+            var lastcriterion = source.all('.level');
+            for (levidx = 0; levidx < lastcriterion.size(); levidx++) {
+                levelsdef[levidx] = lastcriterion.item(levidx).one('.definition .textvalue').get('innerHTML');
+            }
+            for (levidx = 0; levidx < lastcriterion.size(); levidx++) {
+                levelsscores[levidx] = lastcriterion.item(levidx).one('.score input[type=text]').get('value');
+            }
+        }
+
+        for (levidx; levidx < 3; levidx++) { levelsscores[levidx] = parseFloat(levelsscores[levidx-1]) + 1; }
+        var levelsstr = '';
+        for (levidx = 0; levidx < levelsscores.length; levidx++) {
+            levelsstr += M.gradingform_rubriceditor.templates[name].level
+                            .replace(/\{LEVEL-id\}/g, 'NEWID'+(newlevid+levidx))
+                            .replace(/\{LEVEL-score\}/g, levelsscores[levidx])
+                            .replace(/\{LEVEL-definition\}/g, levelsdef[levidx]);
+        }
+        var description = source.one('.description .textvalue');
+        var newcriterion = M.gradingform_rubriceditor.templates[name].criterion
+                                .replace(/\{LEVELS\}/, levelsstr)
+                                .replace(/\{CRITERION-description\}/, description.get('innerHTML'));
+        parentel.append(newcriterion.replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, ''));
+        M.gradingform_rubriceditor.assignclasses('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-levels .level');
+        M.gradingform_rubriceditor.addhandlers();
+        M.gradingform_rubriceditor.disablealleditors();
+        M.gradingform_rubriceditor.assignclasses(elements_str);
+        M.gradingform_rubriceditor.editmode(Y.one('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-description'),true);
     } else if (chunks.length == 6 && action == 'delete') {
         // DELETE LEVEL
         if (confirmed) {
@@ -241,4 +276,4 @@ M.gradingform_rubriceditor.calculatenewid = function (elements_str) {
         if (id.match(/^NEWID(\d+)$/)) newid = Math.max(newid, parseInt(id.substring(5))+1);
     } );
     return newid
-}
\ No newline at end of file
+}
index 9e45bfc..f23b914 100644 (file)
@@ -31,6 +31,7 @@ $string['confirmdeletecriterion'] = 'Are you sure you want to delete this criter
 $string['confirmdeletelevel'] = 'Are you sure you want to delete this level?';
 $string['criterionaddlevel'] = 'Add level';
 $string['criteriondelete'] = 'Delete criterion';
+$string['criterionduplicate'] = 'Duplicate criterion';
 $string['criterionempty'] = 'Click to edit criterion';
 $string['criterionmovedown'] = 'Move down';
 $string['criterionmoveup'] = 'Move up';
index b2ba8ff..c539535 100644 (file)
@@ -71,7 +71,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         $criteriontemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $criterion['class'], 'id' => '{NAME}-criteria-{CRITERION-id}'));
         if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
             $criteriontemplate .= html_writer::start_tag('td', array('class' => 'controls'));
-            foreach (array('moveup', 'delete', 'movedown') as $key) {
+            foreach (array('moveup', 'delete', 'movedown', 'duplicate') as $key) {
                 $value = get_string('criterion'.$key, 'gradingform_rubric');
                 $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}]['.$key.']',
                     'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value, 'title' => $value, 'tabindex' => -1));
index 35b22bf..b889762 100644 (file)
 
 /* replace buttons with images */
 .gradingform_rubric.editor .delete input,
+.gradingform_rubric.editor .duplicate input,
 .gradingform_rubric.editor .moveup input,
 .gradingform_rubric.editor .movedown input{text-indent: -1000em;cursor:pointer;border:none;}
 .gradingform_rubric.editor .criterion .controls .delete input {width:12px;height:12px;background: transparent url([[pix:t/delete]]) no-repeat center top; margin: .3em .3em 0 .3em;}
+.gradingform_rubric.editor .criterion .controls .duplicate input {width:12px;height:12px;background: transparent url([[pix:t/copy]]) no-repeat center top; margin: .3em .3em 0 .3em;}
 .gradingform_rubric.editor .levels .level .delete input {width:12px;height:16px;background: transparent url([[pix:t/delete]]) no-repeat center center; }
 .dir-rtl .gradingform_rubric.editor .levels .level .delete input { margin-right: .45em; margin-left: 0; }
 .gradingform_rubric.editor .moveup input {width:12px;height:12px;background: transparent url([[pix:t/up]]) no-repeat center top; margin: .3em .3em 0 .3em; }
index 8e4bc80..01f13c6 100644 (file)
@@ -25,7 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component  = 'gradingform_rubric';
-$plugin->version    = 2015051100;
+$plugin->version    = 2015051101;
 
 $plugin->requires   = 2015050500;
 
index b78f801..84a8512 100644 (file)
@@ -515,6 +515,15 @@ function hide_aggregatesubcats_upgrade_notice($courseid) {
     unset_config('show_aggregatesubcats_upgrade_' . $courseid);
 }
 
+/**
+ * Hide warning about changed grades due to bug fixes
+ *
+ * @param int $courseid The current course id.
+ */
+function hide_gradebook_calculations_freeze_notice($courseid) {
+    unset_config('gradebook_calculations_freeze_' . $courseid);
+}
+
 /**
  * Print warning about changed grades during upgrade to 2.8.
  *
@@ -547,6 +556,9 @@ function print_natural_aggregation_upgrade_notice($courseid, $context, $thispage
 
     $minmaxtouse = grade_get_setting($courseid, 'minmaxtouse', $CFG->grade_minmaxtouse);
 
+    $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $courseid);
+    $acceptgradebookchanges = optional_param('acceptgradebookchanges', false, PARAM_BOOL) && confirm_sesskey();
+
     // Hide the warning if the user told it to go away.
     if ($hidenaturalwarning) {
         hide_natural_aggregation_upgrade_notice($courseid);
@@ -622,7 +634,7 @@ function print_natural_aggregation_upgrade_notice($courseid, $context, $thispage
             $reverturl = new moodle_url($thispage, $urlparams);
             $revertbutton = $OUTPUT->single_button($reverturl, $revertmessage, 'get');
 
-            $html .= $OUTPUT->notification($message, 'notifywarning');
+            $html .= $OUTPUT->notification($message);
             $html .= $revertbutton . $hideminmaxbutton;
 
         } else if ($minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE) {
@@ -636,11 +648,44 @@ function print_natural_aggregation_upgrade_notice($courseid, $context, $thispage
             $fixurl = new moodle_url($thispage, $urlparams);
             $fixbutton = $OUTPUT->single_button($fixurl, $fixmessage, 'get');
 
-            $html .= $OUTPUT->notification($message, 'notifywarning');
+            $html .= $OUTPUT->notification($message);
             $html .= $fixbutton . $hideminmaxbutton;
         }
     }
 
+    if ($gradebookcalculationsfreeze) {
+        if ($acceptgradebookchanges) {
+            // Accept potential changes in grades caused by extra credit bug MDL-49257.
+            hide_gradebook_calculations_freeze_notice($courseid);
+            $courseitem = grade_item::fetch_course_item($courseid);
+            $courseitem->force_regrading();
+            grade_regrade_final_grades($courseid);
+
+            $html .= $OUTPUT->notification(get_string('gradebookcalculationsuptodate', 'grades'), 'notifysuccess');
+        } else {
+            // Show the warning that there may be extra credit weights problems.
+            $a = new stdClass();
+            $a->gradebookversion = $gradebookcalculationsfreeze;
+            if (preg_match('/(\d{8,})/', $CFG->release, $matches)) {
+                $a->currentversion = $matches[1];
+            } else {
+                $a->currentversion = $CFG->release;
+            }
+            $a->url = get_docs_url('Gradebook_calculation_changes');
+            $message = get_string('gradebookcalculationswarning', 'grades', $a);
+
+            $fixmessage = get_string('gradebookcalculationsfixbutton', 'grades');
+            $urlparams = array('id' => $courseid,
+                'acceptgradebookchanges' => true,
+                'sesskey' => sesskey());
+            $fixurl = new moodle_url($thispage, $urlparams);
+            $fixbutton = $OUTPUT->single_button($fixurl, $fixmessage, 'get');
+
+            $html .= $OUTPUT->notification($message);
+            $html .= $fixbutton;
+        }
+    }
+
     if (!empty($html)) {
         $html = html_writer::tag('div', $html, array('class' => 'core_grades_notices'));
     }
index c402f82..244af2f 100644 (file)
@@ -589,6 +589,9 @@ class grade_report_grader extends grade_report {
         $rows = array();
 
         $showuserimage = $this->get_pref('showuserimage');
+        $canseeuserreport = has_capability('gradereport/'.$CFG->grade_profilereport.':view', $this->context);
+        $canseesingleview = has_capability('gradereport/singleview:view', $this->context);
+        $hasuserreportcell = $canseeuserreport || $canseesingleview;
 
         $strfeedback  = $this->get_lang_string("feedback");
         $strgrade     = $this->get_lang_string('grade');
@@ -597,11 +600,7 @@ class grade_report_grader extends grade_report {
 
         $arrows = $this->get_sort_arrows($extrafields);
 
-        $colspan = 1;
-        if (has_capability('gradereport/'.$CFG->grade_profilereport.':view', $this->context)) {
-            $colspan++;
-        }
-        $colspan += count($extrafields);
+        $colspan = 1 + $hasuserreportcell + count($extrafields);
 
         $levels = count($this->gtree->levels) - 1;
 
@@ -628,7 +627,7 @@ class grade_report_grader extends grade_report {
         $studentheader->scope = 'col';
         $studentheader->header = true;
         $studentheader->id = 'studentheader';
-        if (has_capability('gradereport/'.$CFG->grade_profilereport.':view', $this->context)) {
+        if ($hasuserreportcell) {
             $studentheader->colspan = 2;
         }
         $studentheader->text = $arrows['studentname'];
@@ -685,7 +684,7 @@ class grade_report_grader extends grade_report {
             $userreportcell = new html_table_cell();
             $userreportcell->attributes['class'] = 'userreport';
             $userreportcell->header = false;
-            if (has_capability('gradereport/'.$CFG->grade_profilereport.':view', $this->context)) {
+            if ($canseeuserreport) {
                 $a = new stdClass();
                 $a->user = $fullname;
                 $strgradesforuser = get_string('gradesforuser', 'grades', $a);
@@ -693,7 +692,7 @@ class grade_report_grader extends grade_report {
                 $userreportcell->text .= $OUTPUT->action_icon($url, new pix_icon('t/grades', $strgradesforuser));
             }
 
-            if (has_capability('gradereport/singleview:view', $this->context)) {
+            if ($canseesingleview) {
                 $url = new moodle_url('/grade/report/singleview/index.php', array('id' => $this->course->id, 'itemid' => $user->id, 'item' => 'user'));
                 $singleview = $OUTPUT->action_icon($url, new pix_icon('t/editstring', get_string('singleview', 'grades', $fullname)));
                 $userreportcell->text .= $singleview;
index db0a75d..d925ef7 100644 (file)
@@ -55,7 +55,7 @@ abstract class grade_report {
 
     /**
      * The grade_tree object.
-     * @var object $gtree
+     * @var grade_tree $gtree
      */
     public $gtree;
 
index 10f3fa1..1c677a3 100644 (file)
@@ -113,12 +113,6 @@ class user extends tablelike implements selectable_items {
         $params = array('courseid' => $this->courseid);
 
         $seq = new grade_seq($this->courseid, true);
-        foreach ($seq->items as $key => $item) {
-            if (isset($item->itemmodule)) {
-                list($courseid, $cmid) = get_course_and_cm_from_instance($item->iteminstance, $item->itemmodule);
-                $seq->items[$key]->cmid = $cmid->id;
-            }
-        }
 
         $this->items = array();
         foreach ($seq->items as $itemid => $item) {
@@ -179,11 +173,6 @@ class user extends tablelike implements selectable_items {
              $lockicon = $OUTPUT->pix_icon('t/locked', 'grade is locked');
         }
 
-        $realmodid = '';
-        if (isset($item->cmid)) {
-            $realmodid = $item->cmid;
-        }
-
         $iconstring = get_string('filtergrades', 'gradereport_singleview', $item->get_name());
 
         // Create a fake gradetreeitem so we can call get_element_header().
@@ -200,12 +189,6 @@ class user extends tablelike implements selectable_items {
         $itemlabel = $this->structure->get_element_header($gradetreeitem, true, false, false, false, true);
         $grade->label = $item->get_name();
 
-        $itemlabel = $item->get_name();
-        if (!empty($realmodid)) {
-            $url = new moodle_url('/mod/' . $item->itemmodule . '/view.php', array('id' => $realmodid));
-            $itemlabel = html_writer::link($url, $item->get_name());
-        }
-
         $line = array(
             $OUTPUT->action_icon($this->format_link('grade', $item->id), new pix_icon('t/editstring', $iconstring)),
             $this->format_icon($item) . $lockicon . $itemlabel,
index 5b5ed81..e9cbcbd 100644 (file)
@@ -107,7 +107,9 @@ class behat_grade extends behat_base {
         $steps[] = new Given('I click on "' . $this->escape($linkxpath) . '" "xpath_element"');
 
         // After adding id numbers we should wait until the page is reloaded.
-        $steps[] = new Given('I wait until the page is ready');
+        if ($this->running_javascript()) {
+            $steps[] = new Given('I wait until the page is ready');
+        }
 
         // Mapping names to idnumbers.
         $datahash = $data->getRowsHash();
@@ -115,9 +117,69 @@ class behat_grade extends behat_base {
             // This xpath looks for course, categories and items with the provided name.
             // Grrr, we can't equal in categoryitem and courseitem because there is a line jump...
             $inputxpath ="//input[@class='idnumber'][" .
+                "parent::li[@class='item'][text()='" . $gradeitem . "']" .
+                " or " .
+                "parent::li[@class='categoryitem' or @class='courseitem']/parent::ul/parent::li[starts-with(text(),'" . $gradeitem . "')]" .
+            "]";
+            $steps[] = new Given('I set the field with xpath "' . $inputxpath . '" to "' . $idnumber . '"');
+        }
+
+        $steps[] = new Given('I press "' . get_string('addidnumbers', 'grades') . '"');
+
+        // After adding id numbers we should wait until the page is reloaded.
+        if ($this->running_javascript()) {
+            $steps[] = new Given('I wait until the page is ready');
+        }
+
+        $steps[] = new Given('I set the field "' . get_string('calculation', 'grades') . '" to "' . $calculation . '"');
+        $steps[] = new Given('I press "' . $savechanges . '"');
+
+        return $steps;
+    }
+
+    /**
+     * Sets a calculated manual grade category total. Needs a table with item name - idnumber relation.
+     * The step requires you to be in categories and items page.
+     *
+     * @Given /^I set "(?P<calculation_string>(?:[^"]|\\")*)" calculation for grade category "(?P<grade_item_string>(?:[^"]|\\")*)" with idnumbers:$/
+     * @param string $calculation The calculation.
+     * @param string $gradeitem The grade item name.
+     * @param TableNode $data The grade item name - idnumbers relation.
+     * @return Given[]
+     */
+    public function i_set_calculation_for_grade_category_with_idnumbers($calculation, $gradeitem, TableNode $data) {
+
+        $steps = array();
+        $gradecategorytotal = $this->getSession()->getSelectorsHandler()->xpathLiteral($gradeitem . ' total');
+        $gradeitem = $this->getSession()->getSelectorsHandler()->xpathLiteral($gradeitem);
+
+        if ($this->running_javascript()) {
+            $xpath = "//tr[contains(.,$gradecategorytotal)]//*[contains(@class,'moodle-actionmenu')]" .
+                "//a[contains(@class,'toggle-display')]";
+            if ($this->getSession()->getPage()->findAll('xpath', $xpath)) {
+                $steps[] = new Given('I click on "' . $this->escape($xpath) . '" "xpath_element"');
+            }
+        }
+
+        // Going to edit calculation.
+        $savechanges = get_string('savechanges', 'grades');
+        $edit = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('editcalculation', 'grades'));
+        $linkxpath = "//a[./img[starts-with(@title,$edit) and contains(@title,$gradeitem)]]";
+        $steps[] = new Given('I click on "' . $this->escape($linkxpath) . '" "xpath_element"');
+
+        // After adding id numbers we should wait until the page is reloaded.
+        $steps[] = new Given('I wait until the page is ready');
+
+        // Mapping names to idnumbers.
+        $datahash = $data->getRowsHash();
+        foreach ($datahash as $gradeitem => $idnumber) {
+            // This xpath looks for course, categories and items with the provided name.
+            // Grrr, we can't equal in categoryitem and courseitem because there is a line jump...
+            $inputxpath = "//input[@class='idnumber'][" .
                 "parent::li[@class='item'][text()='" . $gradeitem . "']" .
                 " | " .
-                "parent::li[@class='categoryitem' | @class='courseitem']/parent::ul/parent::li[starts-with(text(),'" . $gradeitem . "')]" .
+                "parent::li[@class='categoryitem' | @class='courseitem']" .
+                "/parent::ul/parent::li[starts-with(text(),'" . $gradeitem . "')]" .
             "]";
             $steps[] = new Given('I set the field with xpath "' . $inputxpath . '" to "' . $idnumber . '"');
         }
@@ -158,4 +220,18 @@ class behat_grade extends behat_base {
         $steps[] = new Given('I click on "' . $this->escape($linktext) . '" "link"');
         return $steps;
     }
+
+    /**
+     * Step allowing to test before-the-fix behaviour of the gradebook
+     *
+     * @Given /^gradebook calculations for the course "(?P<coursename_string>(?:[^"]|\\")*)" are frozen at version "(?P<version_string>(?:[^"]|\\")*)"$/
+     * @param string $coursename
+     * @param string $version
+     * @return Given
+     */
+    public function gradebook_calculations_for_the_course_are_frozen_at_version($coursename, $version) {
+        global $DB;
+        $courseid = $DB->get_field('course', 'id', array('shortname' => $coursename), MUST_EXIST);
+        set_config('gradebook_calculations_freeze_' . $courseid, $version);
+    }
 }
diff --git a/grade/tests/behat/grade_calculated_grade_items.feature b/grade/tests/behat/grade_calculated_grade_items.feature
new file mode 100644 (file)
index 0000000..e295c02
--- /dev/null
@@ -0,0 +1,164 @@
+@core @core_grades
+Feature: Calculated grade items can be used in the gradebook
+  In order to use calculated grade items in the gradebook
+  As a teacher
+  I need setup calculated grade items in the categories and items page.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email                | idnumber |
+      | teacher1 | Teacher   | 1        | teacher1@example.com | t1       |
+      | student1 | Student   | 1        | student1@example.com | s1       |
+      | student2 | Student   | 2        | student2@example.com | s2       |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+      | student2 | C1     | student        |
+    And I log in as "admin"
+    And I am on site homepage
+    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"
+
+  @javascript
+  Scenario: The max grade for a category item, with a calculation using Natural aggregation, can be changed
+    Given I press "Add category"
+    And I set the following fields to these values:
+      | Category name | Calc cat |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | grade item 1 |
+      | Grade category | Calc cat |
+    And I press "Save changes"
+    And I set "=[[gi1]]/2" calculation for grade category "Calc cat" with idnumbers:
+      | grade item 1 | gi1 |
+    And I set the following settings for grade item "Calc cat":
+      | Maximum grade | 50 |
+    And I follow "Grader report"
+    And I turn editing mode on
+    And I give the grade "75.00" to the user "Student 1" for the grade item "grade item 1"
+    And I press "Save changes"
+    And I follow "User report"
+    And I select "Student 1" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item                          | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1                        | -                 | 75.00  | 0–100 | 75.00 %    | -                            |
+      | Calc cat totalInclude empty grades. | 100.00 %          | 37.50  | 0–50  | 75.00 %    | -                            |
+      | Course total                        | -                 | 37.50  | 0–50  | 75.00 %    | -                            |
+
+  @javascript
+  Scenario: Changing max grade for a category item with a calculation that has existing grades will display the same points with the new max grade values immediately.
+  Given I press "Add category"
+    And I set the following fields to these values:
+      | Category name | Calc cat |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | grade item 1 |
+      | Grade category | Calc cat |
+    And I press "Save changes"
+    And I set "=[[gi1]]/2" calculation for grade category "Calc cat" with idnumbers:
+      | grade item 1 | gi1 |
+    And I set the following settings for grade item "Calc cat":
+      | Maximum grade | 50 |
+    And I follow "Grader report"
+    And I turn editing mode on
+    And I give the grade "75.00" to the user "Student 1" for the grade item "grade item 1"
+    And I press "Save changes"
+    And I follow "User report"
+    And I select "Student 1" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item                          | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1                        | -                 | 75.00  | 0–100 | 75.00 %    | -                            |
+      | Calc cat totalInclude empty grades. | 100.00 %          | 37.50  | 0–50  | 75.00 %    | -                            |
+      | Course total                        | -                 | 37.50  | 0–50  | 75.00 %    | -                            |
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I set the following settings for grade item "Calc cat":
+      | Maximum grade | 40 |
+    And I follow "Grader report"
+    And I give the grade "65.00" to the user "Student 2" for the grade item "grade item 1"
+    And I press "Save changes"
+    And I follow "User report"
+    When I select "Student 1" from the "Select all or one user" singleselect
+    Then the following should exist in the "user-grade" table:
+      | Grade item                          | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1                        | -                 | 75.00  | 0–100 | 75.00 %    | -                            |
+      | Calc cat totalInclude empty grades. | 100.00 %          | 37.50  | 0–40  | 93.75 %    | -                            |
+      | Course total                        | -                 | 37.50  | 0–40  | 93.75 %    | -                            |
+    And I select "Student 2" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item                          | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1                        | -                 | 65.00  | 0–100 | 65.00 %    | -                            |
+      | Calc cat totalInclude empty grades. | 100.00 %          | 32.50  | 0–40  | 81.25 %    | -                            |
+      | Course total                        | -                 | 32.50  | 0–40  | 81.25 %    | -                            |
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
+    And I set the following fields to these values:
+      | Min and max grades used in calculation | Initial min and max grades |
+    And I press "Save changes"
+    And I follow "User report"
+    And I select "Student 1" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item                          | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1                        | -                 | 75.00  | 0–100 | 75.00 %    | -                            |
+      | Calc cat totalInclude empty grades. | 100.00 %          | 37.50  | 0–40  | 93.75 %    | -                            |
+      | Course total                        | -                 | 37.50  | 0–40  | 93.75 %    | -                            |
+    And I select "Student 2" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item                          | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1                        | -                 | 65.00  | 0–100 | 65.00 %    | -                            |
+      | Calc cat totalInclude empty grades. | 100.00 %          | 32.50  | 0–40  | 81.25 %    | -                            |
+      | Course total                        | -                 | 32.50  | 0–40  | 81.25 %    | -                            |
+
+  @javascript
+  Scenario: Values in calculated grade items are not always out of one hundred
+    Given I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | grade item 1 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | calc item |
+    And I press "Save changes"
+    And I set "=[[gi1]]/2" calculation for grade item "calc item" with idnumbers:
+      | grade item 1 | gi1 |
+    And I set the following settings for grade item "calc item":
+      | Maximum grade | 50 |
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
+    And I set the following fields to these values:
+      | Min and max grades used in calculation | Initial min and max grades |
+    And I press "Save changes"
+    And I follow "Grader report"
+    And I turn editing mode on
+    And I give the grade "75.00" to the user "Student 1" for the grade item "grade item 1"
+    And I press "Save changes"
+    And I follow "User report"
+    When I select "Student 1" from the "Select all or one user" singleselect
+    Then the following should exist in the "user-grade" table:
+      | Grade item   | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1 | 66.67 %           | 75.00  | 0–100 | 75.00 %    | 50.00 %                      |
+      | calc item    | 33.33 %           | 37.50  | 0–50  | 75.00 %    | 25.00 %                      |
+      | Course total | -                 | 112.50 | 0–150 | 75.00 %    | -                            |
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I set the following settings for grade item "calc item":
+      | Maximum grade | 40 |
+    And I follow "Grader report"
+    And I give the grade "65.00" to the user "Student 2" for the grade item "grade item 1"
+    And I press "Save changes"
+    And I follow "User report"
+    And I select "Student 1" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item   | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1 | 71.43 %           | 75.00  | 0–100 | 75.00 %    | 53.57 %                      |
+      | calc item    | 28.57 %           | 37.50  | 0–40  | 93.75 %    | 26.79 %                      |
+      | Course total | -                 | 112.50 | 0–140 | 80.36 %    | -                            |
+    And I select "Student 2" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item   | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1 | 71.43 %           | 65.00  | 0–100 | 65.00 %    | 46.43 %                      |
+      | calc item    | 28.57 %           | 32.50  | 0–40  | 81.25 %    | 23.21 %                      |
+      | Course total | -                 | 97.50  | 0–140 | 69.64 %    | -                            |
\ No newline at end of file
diff --git a/grade/tests/behat/grade_calculated_grade_items_20150627.feature b/grade/tests/behat/grade_calculated_grade_items_20150627.feature
new file mode 100644 (file)
index 0000000..62d5e3e
--- /dev/null
@@ -0,0 +1,165 @@
+@core @core_grades
+Feature: Gradebook calculations for calculated grade items before the fix 20150627
+  In order to make sure the grades are not changed after upgrade
+  As a teacher
+  I need to be able to freeze gradebook calculations
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And gradebook calculations for the course "C1" are frozen at version "20150627"
+    And the following "users" exist:
+      | username | firstname | lastname | email                | idnumber |
+      | teacher1 | Teacher   | 1        | teacher1@example.com | t1       |
+      | student1 | Student   | 1        | student1@example.com | s1       |
+      | student2 | Student   | 2        | student2@example.com | s2       |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+      | student2 | C1     | student        |
+    And I log in as "admin"
+    And I am on site homepage
+    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"
+
+  @javascript
+  Scenario: The max grade for a category item, with a calculation using Natural aggregation, can be changed
+    Given I press "Add category"
+    And I set the following fields to these values:
+      | Category name | Calc cat |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | grade item 1 |
+      | Grade category | Calc cat |
+    And I press "Save changes"
+    And I set "=[[gi1]]/2" calculation for grade category "Calc cat" with idnumbers:
+      | grade item 1 | gi1 |
+    And I set the following settings for grade item "Calc cat":
+      | Maximum grade | 50 |
+    And I follow "Grader report"
+    And I turn editing mode on
+    And I give the grade "75.00" to the user "Student 1" for the grade item "grade item 1"
+    And I press "Save changes"
+    And I follow "User report"
+    And I select "Student 1" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item                          | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1                        | -                 | 75.00  | 0–100 | 75.00 %    | -                            |
+      | Calc cat totalInclude empty grades. | 100.00 %          | 37.50  | 0–100 | 37.50 %    | -                            |
+      | Course total                        | -                 | 37.50  | 0–100 | 37.50 %    | -                            |
+
+  @javascript
+  Scenario: Changing max grade for a category item with a calculation that has existing grades will display the same points with the new max grade values immediately.
+    Given I press "Add category"
+    And I set the following fields to these values:
+      | Category name | Calc cat |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | grade item 1 |
+      | Grade category | Calc cat |
+    And I press "Save changes"
+    And I set "=[[gi1]]/2" calculation for grade category "Calc cat" with idnumbers:
+      | grade item 1 | gi1 |
+    And I set the following settings for grade item "Calc cat":
+      | Maximum grade | 50 |
+    And I follow "Grader report"
+    And I turn editing mode on
+    And I give the grade "75.00" to the user "Student 1" for the grade item "grade item 1"
+    And I press "Save changes"
+    And I follow "User report"
+    And I select "Student 1" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item                          | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1                        | -                 | 75.00  | 0–100 | 75.00 %    | -                            |
+      | Calc cat totalInclude empty grades. | 100.00 %          | 37.50  | 0–100 | 37.50 %    | -                            |
+      | Course total                        | -                 | 37.50  | 0–100 | 37.50 %    | -                            |
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I set the following settings for grade item "Calc cat":
+      | Maximum grade | 40 |
+    And I follow "Grader report"
+    And I give the grade "65.00" to the user "Student 2" for the grade item "grade item 1"
+    And I press "Save changes"
+    And I follow "User report"
+    When I select "Student 1" from the "Select all or one user" singleselect
+    Then the following should exist in the "user-grade" table:
+      | Grade item                          | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1                        | -                 | 75.00  | 0–100 | 75.00 %    | -                            |
+      | Calc cat totalInclude empty grades. | 100.00 %          | 37.50  | 0–100 | 37.50 %    | -                            |
+      | Course total                        | -                 | 37.50  | 0–100 | 37.50 %    | -                            |
+    And I select "Student 2" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item                          | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1                        | -                 | 65.00  | 0–100 | 65.00 %    | -                            |
+      | Calc cat totalInclude empty grades. | 100.00 %          | 32.50  | 0–100 | 32.50 %    | -                            |
+      | Course total                        | -                 | 32.50  | 0–100 | 32.50 %    | -                            |
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
+    And I set the following fields to these values:
+      | Min and max grades used in calculation | Initial min and max grades |
+    And I press "Save changes"
+    And I follow "User report"
+    And I select "Student 1" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item                          | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1                        | -                 | 75.00  | 0–100 | 75.00 %    | -                            |
+      | Calc cat totalInclude empty grades. | 100.00 %          | 37.50  | 0–100 | 37.50 %    | -                            |
+      | Course total                        | -                 | 37.50  | 0–100 | 37.50 %    | -                            |
+    And I select "Student 2" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item                          | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1                        | -                 | 65.00  | 0–100 | 65.00 %    | -                            |
+      | Calc cat totalInclude empty grades. | 100.00 %          | 32.50  | 0–100 | 32.50 %    | -                            |
+      | Course total                        | -                 | 32.50  | 0–100 | 32.50 %    | -                            |
+
+  @javascript
+  Scenario: Values in calculated grade items are not always out of one hundred
+    Given I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | grade item 1 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | calc item |
+    And I press "Save changes"
+    And I set "=[[gi1]]/2" calculation for grade item "calc item" with idnumbers:
+      | grade item 1 | gi1 |
+    And I set the following settings for grade item "calc item":
+      | Maximum grade | 50 |
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
+    And I set the following fields to these values:
+      | Min and max grades used in calculation | Initial min and max grades |
+    And I press "Save changes"
+    And I follow "Grader report"
+    And I turn editing mode on
+    And I give the grade "75.00" to the user "Student 1" for the grade item "grade item 1"
+    And I press "Save changes"
+    And I follow "User report"
+    When I select "Student 1" from the "Select all or one user" singleselect
+    Then the following should exist in the "user-grade" table:
+      | Grade item   | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1 | 50.00 %           | 75.00  | 0–100 | 75.00 %    | 37.50 %                      |
+      | calc item    | 50.00 %           | 37.50  | 0–100 | 37.50 %    | 18.75 %                      |
+      | Course total | -                 | 112.50 | 0–200 | 56.25 %    | -                            |
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I set the following settings for grade item "calc item":
+      | Maximum grade | 40 |
+    And I follow "Grader report"
+    And I give the grade "65.00" to the user "Student 2" for the grade item "grade item 1"
+    And I press "Save changes"
+    And I follow "User report"
+    And I select "Student 1" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item   | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1 | 50.00 %           | 75.00  | 0–100 | 75.00 %    | 37.50 %                      |
+      | calc item    | 50.00 %           | 37.50  | 0–100 | 37.50 %    | 18.75 %                      |
+      | Course total | -                 | 112.50 | 0–200 | 56.25 %    | -                            |
+    And I select "Student 2" from the "Select all or one user" singleselect
+    And the following should exist in the "user-grade" table:
+      | Grade item   | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
+      | grade item 1 | 50.00 %           | 65.00  | 0–100 | 65.00 %    | 32.50 %                      |
+      | calc item    | 50.00 %           | 32.50  | 0–100 | 32.50 %    | 16.25 %                      |
+      | Course total | -                 | 97.50  | 0–200 | 48.75 %    | -                            |
\ No newline at end of file
index 2102bfd..b956a5a 100644 (file)
@@ -4,7 +4,6 @@ Feature: We can choose what min or max grade to use when aggregating grades.
   As an teacher
   I can update modify a course setting
 
-  @javascript
   Scenario: Changing the min or max grade to use updates the grades accordingly
     Given the following "courses" exist:
       | fullname | shortname | category | groupmode |
@@ -148,20 +147,20 @@ Feature: We can choose what min or max grade to use when aggregating grades.
     And I select "Student 1" from the "Select all or one user" singleselect
     Then the following should exist in the "user-grade" table:
       | Grade item   | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
-      | MI 1         | 20.00 %           | 75.00  | 0–100 | 75.00 %    | 15.00 %                      |
-      | MI 2         | 20.00 %           | 25.00  | 0–100 | 25.00 %    | 5.00 %                       |
-      | MI 3         | 50.00 %           | 50.00  | 0–100 | 50.00 %    | 10.00 %                      |
-      | MI 4         | 50.00 %           | 100.00 | 0–100 | 100.00 %   | 20.00 %                      |
-      | MI 5         | 20.00 %           | 150.00 | 0–100 | 100.00 %   | 30.00 %                      |
-      | CAT1 total   | 40.00 %           | 150.00 | 0–200 | 75.00 %    | -                            |
-      | Course total | -                 | 400.00 | 0–500 | 80.00 %    | -                            |
+      | MI 1         | 16.67 %           | 75.00  | 0–100 | 75.00 %    | 12.50 %                      |
+      | MI 2         | 16.67 %           | 25.00  | 0–100 | 25.00 %    | 4.17 %                       |
+      | MI 3         | 50.00 %           | 50.00  | 0–100 | 50.00 %    | 8.33 %                       |
+      | MI 4         | 50.00 %           | 100.00 | 0–100 | 100.00 %   | 16.67 %                      |
+      | MI 5         | 33.33 %           | 150.00 | 0–200 | 75.00 %    | 25.00 %                      |
+      | CAT1 total   | 33.33 %           | 150.00 | 0–200 | 75.00 %    | -                            |
+      | Course total | -                 | 400.00 | 0–600 | 66.67 %    | -                            |
     And I select "Student 2" from the "Select all or one user" singleselect
     And the following should exist in the "user-grade" table:
       | Grade item   | Calculated weight | Grade  | Range | Percentage | Contribution to course total |
-      | MI 1         | 33.33 %           | 20.00  | 0–100 | 20.00 %    | 6.67 %                       |
+      | MI 1         | 25.00 %           | 20.00  | 0–100 | 20.00 %    | 5.00 %                       |
       | MI 2         | 0.00 %            | -      | 0–100 | -          | 0.00 %                       |
-      | MI 3         | 100.00 %          | 10.00  | 0–100 | 10.00 %    | 3.33 %                       |
+      | MI 3         | 100.00 %          | 10.00  | 0–100 | 10.00 %    | 2.50 %                       |
       | MI 4         | 0.00 %            | -      | 0–100 | -          | 0.00 %                       |
-      | MI 5         | 33.33 %           | 30.00  | 0–100 | 30.00 %    | 10.00 %                      |
-      | CAT1 total   | 33.33 %           | 10.00  | 0–100 | 10.00 %    | -                            |
-      | Course total | -                 | 60.00  | 0–300 | 20.00 %    | -                            |
+      | MI 5         | 50.00 %           | 30.00  | 0–200 | 15.00 %    | 7.50 %                       |
+      | CAT1 total   | 25.00 %           | 10.00  | 0–100 | 10.00 %    | -                            |
+      | Course total | -                 | 60.00  | 0–400 | 15.00 %    | -                            |
diff --git a/grade/tests/behat/grade_natural_exclude_empty.feature b/grade/tests/behat/grade_natural_exclude_empty.feature
new file mode 100644 (file)
index 0000000..de2f83a
--- /dev/null
@@ -0,0 +1,270 @@
+@core @core_grades
+Feature: Weights in natural aggregation are adjusted if the items are excluded from user report
+  In order to correctly display user report
+  As a teacher
+  I need to be able to exclude hidden grades.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email                | idnumber |
+      | teacher1 | Teacher   | 1        | teacher1@example.com | t1       |
+      | student1 | Student   | 1        | student1@example.com | s1       |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And the following "activities" exist:
+      | activity | course | idnumber | name                         | intro | grade |
+      | assign   | C1     | a1       | Test assignment one          | x     | 100   |
+      | assign   | C1     | a2       | Test assignment two          | x     | 50    |
+      | assign   | C1     | a3       | Test assignment three        | x     | 200   |
+      | assign   | C1     | a4       | Test assignment four (extra) | x     | 20    |
+      | assign   | C1     | a5       | Test assignment five (extra) | x     | 10    |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I set the field "Grade report" to "Categories and items"
+    And I set the following settings for grade item "Test assignment four (extra)":
+      | Extra credit | 1 |
+    And I set the following settings for grade item "Test assignment five (extra)":
+      | Extra credit | 1 |
+
+  @javascript
+  Scenario: No weights are overridden and student has all grades present
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "150.00" to the user "Student 1" for the grade item "Test assignment three"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight      | Grade  | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 28.57 %                | 80.00  | 0–100 | 80.00 %    | 22.86 %                      |
+      | Test assignment two          | 14.29 %                | 30.00  | 0–50  | 60.00 %    | 8.57 %                       |
+      | Test assignment three        | 57.14 %                | 150.00 | 0–200 | 75.00 %    | 42.86 %                      |
+      | Test assignment four (extra) | 5.71 %( Extra credit ) | 10.00  | 0–20  | 50.00 %    | 2.86 %                       |
+      | Test assignment five (extra) | 2.86 %( Extra credit ) | 8.00   | 0–10  | 80.00 %    | 2.29 %                       |
+      | Course total                 | -                      | 278.00 | 0–350 | 79.43 %    | -                            |
+    And I log out
+
+  @javascript
+  Scenario: No weights are overridden, student has some grades present
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight       | Grade  | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 66.67 %                 | 80.00  | 0–100 | 80.00 %    | 53.33 %                      |
+      | Test assignment two          | 33.33 %                 | 30.00  | 0–50  | 60.00 %    | 20.00 %                      |
+      | Test assignment three        | 0.00 %( Empty )         | -      | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 13.33 %( Extra credit ) | 10.00  | 0–20  | 50.00 %    | 6.67 %                       |
+      | Test assignment five (extra) | 6.67 %( Extra credit )  | 8.00   | 0–10  | 80.00 %    | 5.33 %                       |
+      | Course total                 | -                       | 128.00 | 0–150 | 85.33 %    | -                            |
+    And I log out
+
+  @javascript
+  Scenario: No weights are overridden, student has none grades present except for extra credit
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight      | Grade | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 0.00 %( Empty )        | -     | 0–100 | -          | 0.00 %                       |
+      | Test assignment two          | 0.00 %( Empty )        | -     | 0–50  | -          | 0.00 %                       |
+      | Test assignment three        | 0.00 %( Empty )        | -     | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 0.00 %( Extra credit ) | 10.00 | 0–20  | 50.00 %    | 0.00 %                       |
+      | Test assignment five (extra) | 0.00 %( Extra credit ) | 8.00  | 0–10  | 80.00 %    | 0.00 %                       |
+      | Course total                 | -                      | 0.00  | 0–0   |            | -                            |
+    And I log out
+
+  @javascript
+  Scenario: Make sure there are no errors when all items are marked as extra credit
+    And I set the following settings for grade item "Test assignment one":
+      | Extra credit | 1 |
+    And I set the following settings for grade item "Test assignment two":
+      | Extra credit | 1 |
+    And I set the following settings for grade item "Test assignment three":
+      | Extra credit | 1 |
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight      | Grade | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 0.00 %( Extra credit ) | 80.00 | 0–100 | 80.00 %    | 0.00 %                       |
+      | Test assignment two          | 0.00 %( Extra credit ) | 30.00 | 0–50  | 60.00 %    | 0.00 %                       |
+      | Test assignment three        | 0.00 %( Empty )        | -     | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 0.00 %( Extra credit ) | 10.00 | 0–20  | 50.00 %    | 0.00 %                       |
+      | Test assignment five (extra) | 0.00 %( Extra credit ) | 8.00  | 0–10  | 80.00 %    | 0.00 %                       |
+      | Course total                 | -                      | 0.0   | 0–0   |            | -                            |
+    And I log out
+
+  @javascript
+  Scenario: Weights are overridden and student has all grades present
+    When I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "50"
+    And I press "Save changes"
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "150.00" to the user "Student 1" for the grade item "Test assignment three"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight      | Grade  | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 50.00 %                | 80.00  | 0–100 | 80.00 %    | 40.00 %                      |
+      | Test assignment two          | 10.00 %                | 30.00  | 0–50  | 60.00 %    | 6.00 %                       |
+      | Test assignment three        | 40.00 %                | 150.00 | 0–200 | 75.00 %    | 30.00 %                      |
+      | Test assignment four (extra) | 5.71 %( Extra credit ) | 10.00  | 0–20  | 50.00 %    | 2.86 %                       |
+      | Test assignment five (extra) | 2.86 %( Extra credit ) | 8.00   | 0–10  | 80.00 %    | 2.29 %                       |
+      | Course total                 | -                      | 284.00 | 0–350 | 81.14 %    | -                            |
+    And I log out
+
+  @javascript
+  Scenario: Weights are overridden and student has some grades present
+    When I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "50"
+    And I press "Save changes"
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight       | Grade  | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 83.33 %                 | 80.00  | 0–100 | 80.00 %    | 66.67 %                      |
+      | Test assignment two          | 16.67 %                 | 30.00  | 0–50  | 60.00 %    | 10.00 %                      |
+      | Test assignment three        | 0.00 %( Empty )         | -      | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 13.33 %( Extra credit ) | 10.00  | 0–20  | 50.00 %    | 6.67 %                       |
+      | Test assignment five (extra) | 6.67 %( Extra credit )  | 8.00   | 0–10  | 80.00 %    | 5.33 %                       |
+      | Course total                 | -                       | 133.00 | 0–150 | 88.67 %    | -                            |
+    And I log out
+
+  @javascript
+  Scenario: Weights are overridden, student has none grades present except for extra credit
+    When I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "50"
+    And I press "Save changes"
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight      | Grade | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 0.00 %( Empty )        | -     | 0–100 | -          | 0.00 %                       |
+      | Test assignment two          | 0.00 %( Empty )        | -     | 0–50  | -          | 0.00 %                       |
+      | Test assignment three        | 0.00 %( Empty )        | -     | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 0.00 %( Extra credit ) | 10.00 | 0–20  | 50.00 %    | 0.00 %                       |
+      | Test assignment five (extra) | 0.00 %( Extra credit ) | 8.00  | 0–10  | 80.00 %    | 0.00 %                       |
+      | Course total                 | -                      | 0.00  | 0–0   |            | -                            |
+    And I log out
+
+  @javascript
+  Scenario: Weights are overridden, including extra credit, and student has all grades present
+    When I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "50"
+    And I set the field "Override weight of Test assignment four (extra)" to "1"
+    And I set the field "Weight of Test assignment four (extra)" to "10"
+    And I press "Save changes"
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "150.00" to the user "Student 1" for the grade item "Test assignment three"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight       | Grade  | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 50.00 %                 | 80.00  | 0–100 | 80.00 %    | 40.00 %                      |
+      | Test assignment two          | 10.00 %                 | 30.00  | 0–50  | 60.00 %    | 6.00 %                       |
+      | Test assignment three        | 40.00 %                 | 150.00 | 0–200 | 75.00 %    | 30.00 %                      |
+      | Test assignment four (extra) | 10.00 %( Extra credit ) | 10.00  | 0–20  | 50.00 %    | 5.00 %                       |
+      | Test assignment five (extra) | 2.86 %( Extra credit )  | 8.00   | 0–10  | 80.00 %    | 2.29 %                       |
+      | Course total                 | -                       | 291.50 | 0–350 | 83.29 %    | -                            |
+    And I log out
+
+  @javascript
+  Scenario: Weights are overridden, including extra credit, and student has some grades present
+    When I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "50"
+    And I set the field "Override weight of Test assignment four (extra)" to "1"
+    And I set the field "Weight of Test assignment four (extra)" to "10"
+    And I press "Save changes"
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight       | Grade  | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 83.33 %                 | 80.00  | 0–100 | 80.00 %    | 66.67 %                      |
+      | Test assignment two          | 16.67 %                 | 30.00  | 0–50  | 60.00 %    | 10.00 %                      |
+      | Test assignment three        | 0.00 %( Empty )         | -      | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 16.67 %( Extra credit ) | 10.00  | 0–20  | 50.00 %    | 8.33 %                       |
+      | Test assignment five (extra) | 6.67 %( Extra credit )  | 8.00   | 0–10  | 80.00 %    | 5.33 %                       |
+      | Course total                 | -                       | 135.50 | 0–150 | 90.33 %    | -                            |
+    And I log out
+
+  @javascript
+  Scenario: Weights are overridden, including extra credit, student has none grades present except for extra credit
+    When I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "50"
+    And I set the field "Override weight of Test assignment four (extra)" to "1"
+    And I set the field "Weight of Test assignment four (extra)" to "10"
+    And I press "Save changes"
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight      | Grade | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 0.00 %( Empty )        | -     | 0–100 | -          | 0.00 %                       |
+      | Test assignment two          | 0.00 %( Empty )        | -     | 0–50  | -          | 0.00 %                       |
+      | Test assignment three        | 0.00 %( Empty )        | -     | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 0.00 %( Extra credit ) | 10.00 | 0–20  | 50.00 %    | 0.00 %                       |
+      | Test assignment five (extra) | 0.00 %( Extra credit ) | 8.00  | 0–10  | 80.00 %    | 0.00 %                       |
+      | Course total                 | -                      | 0.00  | 0–0   |            | -                            |
+    And I log out
diff --git a/grade/tests/behat/grade_natural_exclude_empty_20150619.feature b/grade/tests/behat/grade_natural_exclude_empty_20150619.feature
new file mode 100644 (file)
index 0000000..b4b97cf
--- /dev/null
@@ -0,0 +1,275 @@
+@core @core_grades
+Feature: Gradebook calculations for extra credit items before the fix 20150619
+  In order to make sure the grades are not changed after upgrade
+  As a teacher
+  I need to be able to freeze gradebook calculations
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And gradebook calculations for the course "C1" are frozen at version "20150619"
+    And the following "users" exist:
+      | username | firstname | lastname | email                | idnumber |
+      | teacher1 | Teacher   | 1        | teacher1@example.com | t1       |
+      | student1 | Student   | 1        | student1@example.com | s1       |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And the following "activities" exist:
+      | activity | course | idnumber | name                         | intro | grade |
+      | assign   | C1     | a1       | Test assignment one          | x     | 100   |
+      | assign   | C1     | a2       | Test assignment two          | x     | 50    |
+      | assign   | C1     | a3       | Test assignment three        | x     | 200   |
+      | assign   | C1     | a4       | Test assignment four (extra) | x     | 20    |
+      | assign   | C1     | a5       | Test assignment five (extra) | x     | 10    |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I set the field "Grade report" to "Categories and items"
+    And I set the following settings for grade item "Test assignment four (extra)":
+      | Extra credit | 1 |
+    And I set the following settings for grade item "Test assignment five (extra)":
+      | Extra credit | 1 |
+
+  @javascript
+  Scenario: No weights are overridden and student has all grades present (before the fix 20150619)
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "150.00" to the user "Student 1" for the grade item "Test assignment three"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight      | Grade  | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 28.57 %                | 80.00  | 0–100 | 80.00 %    | 22.86 %                      |
+      | Test assignment two          | 14.29 %                | 30.00  | 0–50  | 60.00 %    | 8.57 %                       |
+      | Test assignment three        | 57.14 %                | 150.00 | 0–200 | 75.00 %    | 42.86 %                      |
+      | Test assignment four (extra) | 5.71 %( Extra credit ) | 10.00  | 0–20  | 50.00 %    | 2.86 %                       |
+      | Test assignment five (extra) | 2.86 %( Extra credit ) | 8.00   | 0–10  | 80.00 %    | 2.29 %                       |
+      | Course total                 | -                      | 278.00 | 0–350 | 79.43 %    | -                            |
+    And I log out
+
+  @javascript
+  Scenario: No weights are overridden, student has some grades present (before the fix 20150619)
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight       | Grade  | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 66.67 %                 | 80.00  | 0–100 | 80.00 %    | 53.33 %                      |
+      | Test assignment two          | 33.33 %                 | 30.00  | 0–50  | 60.00 %    | 20.00 %                      |
+      | Test assignment three        | 0.00 %( Empty )         | -      | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 13.33 %( Extra credit ) | 10.00  | 0–20  | 50.00 %    | 6.67 %                       |
+      | Test assignment five (extra) | 6.67 %( Extra credit )  | 8.00   | 0–10  | 80.00 %    | 5.33 %                       |
+      | Course total                 | -                       | 128.00 | 0–150 | 85.33 %    | -                            |
+    And I log out
+
+  @javascript
+  Scenario: No weights are overridden, student has none grades present except for extra credit (before the fix 20150619)
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight      | Grade | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 0.00 %( Empty )        | -     | 0–100 | -          | 0.00 %                       |
+      | Test assignment two          | 0.00 %( Empty )        | -     | 0–50  | -          | 0.00 %                       |
+      | Test assignment three        | 0.00 %( Empty )        | -     | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 0.00 %( Extra credit ) | 10.00 | 0–20  | 50.00 %    | 0.00 %                       |
+      | Test assignment five (extra) | 0.00 %( Extra credit ) | 8.00  | 0–10  | 80.00 %    | 0.00 %                       |
+      | Course total                 | -                      | 0.00  | 0–0   |            | -                            |
+    And I log out
+
+  @javascript
+  Scenario: Make sure there are no errors when all items are marked as extra credit (before the fix 20150619)
+    And I set the following settings for grade item "Test assignment one":
+      | Extra credit | 1 |
+    And I set the following settings for grade item "Test assignment two":
+      | Extra credit | 1 |
+    And I set the following settings for grade item "Test assignment three":
+      | Extra credit | 1 |
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight      | Grade | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 0.00 %( Extra credit ) | 80.00 | 0–100 | 80.00 %    | 0.00 %                       |
+      | Test assignment two          | 0.00 %( Extra credit ) | 30.00 | 0–50  | 60.00 %    | 0.00 %                       |
+      | Test assignment three        | 0.00 %( Empty )        | -     | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 0.00 %( Extra credit ) | 10.00 | 0–20  | 50.00 %    | 0.00 %                       |
+      | Test assignment five (extra) | 0.00 %( Extra credit ) | 8.00  | 0–10  | 80.00 %    | 0.00 %                       |
+      | Course total                 | -                      | 0.0   | 0–0   |            | -                            |
+    And I log out
+
+  @javascript
+  Scenario: Weights are overridden and student has all grades present (before the fix 20150619)
+    When I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "50"
+    And I press "Save changes"
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "150.00" to the user "Student 1" for the grade item "Test assignment three"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight      | Grade  | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 50.00 %                | 80.00  | 0–100 | 80.00 %    | 40.00 %                      |
+      | Test assignment two          | 10.00 %                | 30.00  | 0–50  | 60.00 %    | 6.00 %                       |
+      | Test assignment three        | 40.00 %                | 150.00 | 0–200 | 75.00 %    | 30.00 %                      |
+      | Test assignment four (extra) | 4.00 %( Extra credit ) | 10.00  | 0–20  | 50.00 %    | 2.00 %                       |
+      | Test assignment five (extra) | 2.00 %( Extra credit ) | 8.00   | 0–10  | 80.00 %    | 1.60 %                       |
+      | Course total                 | -                      | 278.60 | 0–350 | 79.60 %    | -                            |
+    # Contributions of extra credit "four" should be 20/350=5.71% and "five" 10/350=2.86% (350 is max grade for the course, 20 and 10 are max grades of "four" and "five")
+    And I log out
+
+  @javascript
+  Scenario: Weights are overridden and student has some grades present (before the fix 20150619)
+    When I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "50"
+    And I press "Save changes"
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight       | Grade  | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 83.33 %                 | 80.00  | 0–100 | 80.00 %    | 66.67 %                      |
+      | Test assignment two          | 16.67 %                 | 30.00  | 0–50  | 60.00 %    | 10.00 %                      |
+      | Test assignment three        | 0.00 %( Empty )         | -      | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 6.67 %( Extra credit )  | 10.00  | 0–20  | 50.00 %    | 3.33 %                       |
+      | Test assignment five (extra) | 3.33 %( Extra credit )  | 8.00   | 0–10  | 80.00 %    | 2.67 %                       |
+      | Course total                 | -                       | 124.00 | 0–150 | 82.67 %    | -                            |
+    # Contributions of extra credit "four" should be 20/150=13.33% and "five" 10/150=6.67% (150 is max grade for the course, 20 and 10 are max grades of "four" and "five")
+    And I log out
+
+  @javascript
+  Scenario: Weights are overridden, student has none grades present except for extra credit (before the fix 20150619)
+    When I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "50"
+    And I press "Save changes"
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight      | Grade | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 0.00 %( Empty )        | -     | 0–100 | -          | 0.00 %                       |
+      | Test assignment two          | 0.00 %( Empty )        | -     | 0–50  | -          | 0.00 %                       |
+      | Test assignment three        | 0.00 %( Empty )        | -     | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 0.00 %( Extra credit ) | 10.00 | 0–20  | 50.00 %    | 0.00 %                       |
+      | Test assignment five (extra) | 0.00 %( Extra credit ) | 8.00  | 0–10  | 80.00 %    | 0.00 %                       |
+      | Course total                 | -                      | 0.00  | 0–0   |            | -                            |
+    And I log out
+
+  @javascript
+  Scenario: Weights are overridden, including extra credit, and student has all grades present (before the fix 20150619)
+    When I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "50"
+    And I set the field "Override weight of Test assignment four (extra)" to "1"
+    And I set the field "Weight of Test assignment four (extra)" to "10"
+    And I press "Save changes"
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "150.00" to the user "Student 1" for the grade item "Test assignment three"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight       | Grade  | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 50.00 %                 | 80.00  | 0–100 | 80.00 %    | 40.00 %                      |
+      | Test assignment two          | 8.70 %                  | 30.00  | 0–50  | 60.00 %    | 5.22 %                       |
+      | Test assignment three        | 34.78 %                 | 150.00 | 0–200 | 75.00 %    | 26.09 %                      |
+      | Test assignment four (extra) | 10.00 %( Extra credit ) | 10.00  | 0–20  | 50.00 %    | 5.00 %                       |
+      | Test assignment five (extra) | 1.74 %( Extra credit )  | 8.00   | 0–10  | 80.00 %    | 1.39 %                       |
+      | Course total                 | -                       | 271.93 | 0–350 | 77.70 %    | -                            |
+    # Which is absolutely terrible because weights of normal items do not add up to 100%
+    And I log out
+
+  @javascript
+  Scenario: Weights are overridden, including extra credit, and student has some grades present (before the fix 20150619)
+    When I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "50"
+    And I set the field "Override weight of Test assignment four (extra)" to "1"
+    And I set the field "Weight of Test assignment four (extra)" to "10"
+    And I press "Save changes"
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight       | Grade  | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 83.33 %                 | 80.00  | 0–100 | 80.00 %    | 66.67 %                      |
+      | Test assignment two          | 0.00 %                  | 30.00  | 0–50  | 60.00 %    | 0.00 %                       |
+      | Test assignment three        | 0.00 %( Empty )         | -      | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 16.67 %( Extra credit ) | 10.00  | 0–20  | 50.00 %    | 8.33 %                       |
+      | Test assignment five (extra) | 0.00 %( Extra credit )  | 8.00   | 0–10  | 80.00 %    | 0.00 %                       |
+      | Course total                 | -                       | 112.50 | 0–150 | 75.00 %    | -                            |
+    # This is just ridiculous, the grades for "two" and "five" are 0 without any reason, and sum weight of normal items is not 100% again.
+    And I log out
+
+  @javascript
+  Scenario: Weights are overridden, including extra credit, student has none grades present except for extra credit (before the fix 20150619)
+    When I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "50"
+    And I set the field "Override weight of Test assignment four (extra)" to "1"
+    And I set the field "Weight of Test assignment four (extra)" to "10"
+    And I press "Save changes"
+    When I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment four (extra)"
+    And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)"
+    And I press "Save changes"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item                   | Calculated weight      | Grade | Range | Percentage | Contribution to course total |
+      | Test assignment one          | 0.00 %( Empty )        | -     | 0–100 | -          | 0.00 %                       |
+      | Test assignment two          | 0.00 %( Empty )        | -     | 0–50  | -          | 0.00 %                       |
+      | Test assignment three        | 0.00 %( Empty )        | -     | 0–200 | -          | 0.00 %                       |
+      | Test assignment four (extra) | 0.00 %( Extra credit ) | 10.00 | 0–20  | 50.00 %    | 0.00 %                       |
+      | Test assignment five (extra) | 0.00 %( Extra credit ) | 8.00  | 0–10  | 80.00 %    | 0.00 %                       |
+      | Course total                 | -                      | 0.00  | 0–0   |            | -                            |
+    And I log out
index 9fe47be..bd7ea43 100644 (file)
@@ -97,7 +97,7 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
     And the field "Weight of Test assignment seven" matches value "0.0"
 
   @javascript
-  Scenario: Grade items weights are normalised when all grade item weights are overridden (sum exactly 100). Extra credit is set to zero.
+  Scenario: Grade items weights are not normalised when all grade item weights are overridden (sum exactly 100). Extra credit is set respectful to number of items.
 
     When I set the following settings for grade item "Test assignment seven":
       | Extra credit | 1 |
@@ -110,17 +110,17 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
     And I set the field "Weight of Test assignment six" to "40"
     And I press "Save changes"
 
-    Then I should see "Your weights have been adjusted to total 100."
+    Then I should not see "Your weights have been adjusted to total 100."
     And the field "Weight of Test assignment five" matches value "60.000"
     And the field "Weight of Test assignment six" matches value "40.000"
-    And the field "Weight of Test assignment seven" matches value "0.0"
+    And the field "Weight of Test assignment seven" matches value "50.0"
     And I reset weights for grade category "Sub category 1"
     And the field "Weight of Test assignment five" matches value "66.667"
     And the field "Weight of Test assignment six" matches value "33.333"
     And the field "Weight of Test assignment seven" matches value "50.0"
 
   @javascript
-  Scenario: Grade items weights are normalised when all grade item weights are overridden (sum over 100). Extra credit is set to zero.
+  Scenario: Grade items weights are normalised when all grade item weights are overridden (sum over 100). Extra credit is set respectful to number of items.
 
     When I set the following settings for grade item "Test assignment seven":
       | Extra credit | 1 |
@@ -133,14 +133,14 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
     Then I should see "Your weights have been adjusted to total 100."
     And the field "Weight of Test assignment five" matches value "54.545"
     And the field "Weight of Test assignment six" matches value "45.455"
-    And the field "Weight of Test assignment seven" matches value "0.0"
+    And the field "Weight of Test assignment seven" matches value "50.0"
     And I reset weights for grade category "Sub category 1"
     And the field "Weight of Test assignment five" matches value "66.667"
     And the field "Weight of Test assignment six" matches value "33.333"
     And the field "Weight of Test assignment seven" matches value "50.0"
 
   @javascript
-  Scenario: Grade items weights are normalised when all grade item weights are overridden (sum under 100). Extra credit is set to zero.
+  Scenario: Grade items weights are normalised when all grade item weights are overridden (sum under 100). Extra credit is set respectful to number of items.
 
     When I set the following settings for grade item "Test assignment seven":
       | Extra credit | 1 |
@@ -153,14 +153,14 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
     Then I should see "Your weights have been adjusted to total 100."
     And the field "Weight of Test assignment five" matches value "57.143"
     And the field "Weight of Test assignment six" matches value "42.857"
-    And the field "Weight of Test assignment seven" matches value "0.0"
+    And the field "Weight of Test assignment seven" matches value "50.0"
     And I reset weights for grade category "Sub category 1"
     And the field "Weight of Test assignment five" matches value "66.667"
     And the field "Weight of Test assignment six" matches value "33.333"
     And the field "Weight of Test assignment seven" matches value "50.0"
 
   @javascript
-  Scenario: Grade items weights are normalised when not all grade item weights are overridden. Extra credit is set respectful to non-overridden items.
+  Scenario: Grade items weights are normalised when not all grade item weights are overridden. Extra credit is set respectful to number of items.
 
     When I set the following settings for grade item "Test assignment seven":
       | Extra credit | 1 |
@@ -171,7 +171,7 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
     Then I should see "Your weights have been adjusted to total 100."
     And the field "Weight of Test assignment five" matches value "40.00"
     And the field "Weight of Test assignment six" matches value "60.000"
-    And the field "Weight of Test assignment seven" matches value "90.0"
+    And the field "Weight of Test assignment seven" matches value "50.0"
     And I reset weights for grade category "Sub category 1"
     And the field "Weight of Test assignment five" matches value "66.667"
     And the field "Weight of Test assignment six" matches value "33.333"
diff --git a/grade/tests/behat/grade_natural_normalisation_20150619.feature b/grade/tests/behat/grade_natural_normalisation_20150619.feature
new file mode 100644 (file)
index 0000000..b46f33f
--- /dev/null
@@ -0,0 +1,118 @@
+@core @core_grades
+Feature: Gradebook calculations for natural weights normalisation before the fix 20150619
+  In order to make sure the grades are not changed after upgrade
+  As a teacher
+  I need to be able to freeze gradebook calculations
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And gradebook calculations for the course "C1" are frozen at version "20150619"
+    And the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | t1 |
+      | student1 | Student | 1 | student1@example.com | s1 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And the following "grade categories" exist:
+      | fullname | course |
+      | Sub category 1 | C1 |
+    And the following "activities" exist:
+      | activity | course | idnumber | name | intro | grade |
+      | assign | C1 | a1 | Test assignment one | Submit something! | 300 |
+      | assign | C1 | a2 | Test assignment two | Submit something! | 100 |
+      | assign | C1 | a3 | Test assignment three | Submit something! | 150 |
+      | assign | C1 | a4 | Test assignment four | Submit nothing! | 150 |
+    And the following "activities" exist:
+      | activity | course | idnumber | name | intro | gradecategory | grade |
+      | assign | C1 | a5 | Test assignment five | Submit something! | Sub category 1 | 20 |
+      | assign | C1 | a6 | Test assignment six | Submit something! | Sub category 1 | 10 |
+      | assign | C1 | a7 | Test assignment seven | Submit nothing! | Sub category 1 | 15 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I set the field "Grade report" to "Categories and items"
+
+  @javascript
+  Scenario: Grade items weights are normalised when all grade item weights are overridden (sum exactly 100). Extra credit is set to zero (before the fix 20150619).
+    When I set the following settings for grade item "Test assignment seven":
+      | Extra credit | 1 |
+    And the field "Weight of Test assignment five" matches value "66.667"
+    And the field "Weight of Test assignment six" matches value "33.333"
+    And the field "Weight of Test assignment seven" matches value "50.0"
+    And I set the field "Override weight of Test assignment five" to "1"
+    And I set the field "Override weight of Test assignment six" to "1"
+    And I set the field "Weight of Test assignment five" to "60"
+    And I set the field "Weight of Test assignment six" to "40"
+    And I press "Save changes"
+
+    Then I should see "Your weights have been adjusted to total 100."
+    And the field "Weight of Test assignment five" matches value "60.000"
+    And the field "Weight of Test assignment six" matches value "40.000"
+    And the field "Weight of Test assignment seven" matches value "0.0"
+    # The weight of "seven" should be 15/30=50% (15 is the maxgrade for "seven" and 30 are max grades for this category (max grade of "five" plus max grade of "six")
+    And I reset weights for grade category "Sub category 1"
+    And the field "Weight of Test assignment five" matches value "66.667"
+    And the field "Weight of Test assignment six" matches value "33.333"
+    And the field "Weight of Test assignment seven" matches value "50.0"
+
+  @javascript
+  Scenario: Grade items weights are normalised when all grade item weights are overridden (sum over 100). Extra credit is set to zero (before the fix 20150619).
+    When I set the following settings for grade item "Test assignment seven":
+      | Extra credit | 1 |
+    And I set the field "Override weight of Test assignment five" to "1"
+    And I set the field "Override weight of Test assignment six" to "1"
+    And I set the field "Weight of Test assignment five" to "60"
+    And I set the field "Weight of Test assignment six" to "50"
+    And I press "Save changes"
+
+    Then I should see "Your weights have been adjusted to total 100."
+    And the field "Weight of Test assignment five" matches value "54.545"
+    And the field "Weight of Test assignment six" matches value "45.455"
+    And the field "Weight of Test assignment seven" matches value "0.0"
+    # The weight of "seven" should be 15/30=50% (15 is the maxgrade for "seven" and 30 are max grades for this category (max grade of "five" plus max grade of "six")
+    And I reset weights for grade category "Sub category 1"
+    And the field "Weight of Test assignment five" matches value "66.667"
+    And the field "Weight of Test assignment six" matches value "33.333"
+    And the field "Weight of Test assignment seven" matches value "50.0"
+
+  @javascript
+  Scenario: Grade items weights are normalised when all grade item weights are overridden (sum under 100). Extra credit is set to zero (before the fix 20150619).
+    When I set the following settings for grade item "Test assignment seven":
+      | Extra credit | 1 |
+    And I set the field "Override weight of Test assignment five" to "1"
+    And I set the field "Override weight of Test assignment six" to "1"
+    And I set the field "Weight of Test assignment five" to "40"
+    And I set the field "Weight of Test assignment six" to "30"
+    And I press "Save changes"
+
+    Then I should see "Your weights have been adjusted to total 100."
+    And the field "Weight of Test assignment five" matches value "57.143"
+    And the field "Weight of Test assignment six" matches value "42.857"
+    And the field "Weight of Test assignment seven" matches value "0.0"
+    # The weight of "seven" should be 15/30=50% (15 is the maxgrade for "seven" and 30 are max grades for this category (max grade of "five" plus max grade of "six")
+    And I reset weights for grade category "Sub category 1"
+    And the field "Weight of Test assignment five" matches value "66.667"
+    And the field "Weight of Test assignment six" matches value "33.333"
+    And the field "Weight of Test assignment seven" matches value "50.0"
+
+  @javascript
+  Scenario: Grade items weights are normalised when not all grade item weights are overridden. Extra credit is set respectful to non-overridden items (before the fix 20150619).
+    When I set the following settings for grade item "Test assignment seven":
+      | Extra credit | 1 |
+    And I set the field "Override weight of Test assignment five" to "1"
+    And I set the field "Weight of Test assignment five" to "40"
+    And I press "Save changes"
+
+    Then I should see "Your weights have been adjusted to total 100."
+    And the field "Weight of Test assignment five" matches value "40.00"
+    And the field "Weight of Test assignment six" matches value "60.000"
+    And the field "Weight of Test assignment seven" matches value "90.0"
+    # The weight of "seven" should be 15/30=50% (15 is the maxgrade for "seven" and 30 are max grades for this category (max grade of "five" plus max grade of "six")
+    And I reset weights for grade category "Sub category 1"
+    And the field "Weight of Test assignment five" matches value "66.667"
+    And the field "Weight of Test assignment six" matches value "33.333"
+    And the field "Weight of Test assignment seven" matches value "50.0"
index 45eadb3..1f9b739 100644 (file)
@@ -54,6 +54,7 @@ class core_group_external extends external_api {
                             'description' => new external_value(PARAM_RAW, 'group description text'),
                             'descriptionformat' => new external_format_value('description', VALUE_DEFAULT),
                             'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase', VALUE_OPTIONAL),
+                            'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL)
                         )
                     ), 'List of group object. A group has a courseid, a name, a description and an enrolment key.'
                 )
@@ -87,6 +88,9 @@ class core_group_external extends external_api {
             if ($DB->get_record('groups', array('courseid'=>$group->courseid, 'name'=>$group->name))) {
                 throw new invalid_parameter_exception('Group with the same name already exists in the course');
             }
+            if (!empty($group->idnumber) && $DB->count_records('groups', array('idnumber' => $group->idnumber))) {
+                throw new invalid_parameter_exception('Group with the same idnumber already exists');
+            }
 
             // now security checks
             $context = context_course::instance($group->courseid, IGNORE_MISSING);
@@ -108,6 +112,10 @@ class core_group_external extends external_api {
             if (!isset($group->enrolmentkey)) {
                 $group->enrolmentkey = '';
             }
+            if (!isset($group->idnumber)) {
+                $group->idnumber = '';
+            }
+
             $groups[] = (array)$group;
         }
 
@@ -132,6 +140,7 @@ class core_group_external extends external_api {
                     'description' => new external_value(PARAM_RAW, 'group description text'),
                     'descriptionformat' => new external_format_value('description'),
                     'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'),
+                    'idnumber' => new external_value(PARAM_RAW, 'id number')
                 )
             ), 'List of group object. A group has an id, a courseid, a name, a description and an enrolment key.'
         );
@@ -165,7 +174,7 @@ class core_group_external extends external_api {
         $groups = array();
         foreach ($params['groupids'] as $groupid) {
             // validate params
-            $group = groups_get_group($groupid, 'id, courseid, name, description, descriptionformat, enrolmentkey', MUST_EXIST);
+            $group = groups_get_group($groupid, 'id, courseid, name, idnumber, description, descriptionformat, enrolmentkey', MUST_EXIST);
 
             // now security checks
             $context = context_course::instance($group->courseid, IGNORE_MISSING);
@@ -205,6 +214,7 @@ class core_group_external extends external_api {
                     'description' => new external_value(PARAM_RAW, 'group description text'),
                     'descriptionformat' => new external_format_value('description'),
                     'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'),
+                    'idnumber' => new external_value(PARAM_RAW, 'id number')
                 )
             )
         );
@@ -247,7 +257,7 @@ class core_group_external extends external_api {
         require_capability('moodle/course:managegroups', $context);
 
         $gs = groups_get_all_groups($params['courseid'], 0, 0,
-            'g.id, g.courseid, g.name, g.description, g.descriptionformat, g.enrolmentkey');
+            'g.id, g.courseid, g.name, g.idnumber, g.description, g.descriptionformat, g.enrolmentkey');
 
         $groups = array();
         foreach ($gs as $group) {
@@ -276,6 +286,7 @@ class core_group_external extends external_api {
                     'description' => new external_value(PARAM_RAW, 'group description text'),
                     'descriptionformat' => new external_format_value('description'),
                     'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'),
+                    'idnumber' => new external_value(PARAM_RAW, 'id number')
                 )
             )
         );
@@ -579,7 +590,8 @@ class core_group_external extends external_api {
                             'courseid' => new external_value(PARAM_INT, 'id of course'),
                             'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
                             'description' => new external_value(PARAM_RAW, 'grouping description text'),
-                            'descriptionformat' => new external_format_value('description', VALUE_DEFAULT)
+                            'descriptionformat' => new external_format_value('description', VALUE_DEFAULT),
+                            'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL)
                         )
                     ), 'List of grouping object. A grouping has a courseid, a name and a description.'
                 )
@@ -613,6 +625,9 @@ class core_group_external extends external_api {
             if ($DB->count_records('groupings', array('courseid'=>$grouping->courseid, 'name'=>$grouping->name))) {
                 throw new invalid_parameter_exception('Grouping with the same name already exists in the course');
             }
+            if (!empty($grouping->idnumber) && $DB->count_records('groupings', array('idnumber' => $grouping->idnumber))) {
+                throw new invalid_parameter_exception('Grouping with the same idnumber already exists');
+            }
 
             // Now security checks            .
             $context = context_course::instance($grouping->courseid);
@@ -652,7 +667,8 @@ class core_group_external extends external_api {
                     'courseid' => new external_value(PARAM_INT, 'id of course'),
                     'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
                     'description' => new external_value(PARAM_RAW, 'grouping description text'),
-                    'descriptionformat' => new external_format_value('description')
+                    'descriptionformat' => new external_format_value('description'),
+                    'idnumber' => new external_value(PARAM_RAW, 'id number')
                 )
             ), 'List of grouping object. A grouping has an id, a courseid, a name and a description.'
         );
@@ -673,7 +689,8 @@ class core_group_external extends external_api {
                             'id' => new external_value(PARAM_INT, 'id of grouping'),
                             'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
                             'description' => new external_value(PARAM_RAW, 'grouping description text'),
-                            'descriptionformat' => new external_format_value('description', VALUE_DEFAULT)
+                            'descriptionformat' => new external_format_value('description', VALUE_DEFAULT),
+                            'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL)
                         )
                     ), 'List of grouping object. A grouping has a courseid, a name and a description.'
                 )
@@ -712,6 +729,11 @@ class core_group_external extends external_api {
                     $DB->count_records('groupings', array('courseid'=>$currentgrouping->courseid, 'name'=>$grouping->name))) {
                 throw new invalid_parameter_exception('A different grouping with the same name already exists in the course');
             }
+            // Check if the new modified grouping idnumber already exists.
+            if (!empty($grouping->idnumber) && $grouping->idnumber != $currentgrouping->idnumber &&
+                    $DB->count_records('groupings', array('idnumber' => $grouping->idnumber))) {
+                throw new invalid_parameter_exception('A different grouping with the same idnumber already exists');
+            }
 
             $grouping->courseid = $currentgrouping->courseid;
 
@@ -817,6 +839,7 @@ class core_group_external extends external_api {
                         $context->id, 'group', 'description', $grouprecord->groupid);
                         $groups[] = array('id' => $grouprecord->groupid,
                                           'name' => $grouprecord->name,
+                                          'idnumber' => $grouprecord->idnumber,
                                           'description' => $grouprecord->description,
                                           'descriptionformat' => $grouprecord->descriptionformat,
                                           'enrolmentkey' => $grouprecord->enrolmentkey,
@@ -847,6 +870,7 @@ class core_group_external extends external_api {
                     'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
                     'description' => new external_value(PARAM_RAW, 'grouping description text'),
                     'descriptionformat' => new external_format_value('description'),
+                    'idnumber' => new external_value(PARAM_RAW, 'id number'),
                     'groups' => new external_multiple_structure(
                         new external_single_structure(
                             array(
@@ -855,7 +879,8 @@ class core_group_external extends external_api {
                                 'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
                                 'description' => new external_value(PARAM_RAW, 'group description text'),
                                 'descriptionformat' => new external_format_value('description'),
-                                'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase')
+                                'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'),
+                                'idnumber' => new external_value(PARAM_RAW, 'id number')
                             )
                         ),
                     'optional groups', VALUE_OPTIONAL)
@@ -932,7 +957,8 @@ class core_group_external extends external_api {
                     'courseid' => new external_value(PARAM_INT, 'id of course'),
                     'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
                     'description' => new external_value(PARAM_RAW, 'grouping description text'),
-                    'descriptionformat' => new external_format_value('description')
+                    'descriptionformat' => new external_format_value('description'),
+                    'idnumber' => new external_value(PARAM_RAW, 'id number')
                 )
             )
         );
@@ -1222,7 +1248,7 @@ class core_group_external extends external_api {
 
         $usergroups = array();
         if (empty($warnings)) {
-            $groups = groups_get_all_groups($course->id, $user->id, 0, 'g.id, g.name, g.description, g.descriptionformat');
+            $groups = groups_get_all_groups($course->id, $user->id, 0, 'g.id, g.name, g.description, g.descriptionformat, g.idnumber');
 
             foreach ($groups as $group) {
                 list($group->description, $group->descriptionformat) =
@@ -1254,7 +1280,8 @@ class core_group_external extends external_api {
                             'id' => new external_value(PARAM_INT, 'group record id'),
                             'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
                             'description' => new external_value(PARAM_RAW, 'group description text'),
-                            'descriptionformat' => new external_format_value('description')
+                            'descriptionformat' => new external_format_value('description'),
+                            'idnumber' => new external_value(PARAM_RAW, 'id number')
                         )
                     )
                 ),
index a65d930..7dc9c65 100644 (file)
@@ -50,6 +50,7 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
         $group1['description'] = 'Group Test 1 description';
         $group1['descriptionformat'] = FORMAT_MOODLE;
         $group1['enrolmentkey'] = 'Test group enrol secret phrase';
+        $group1['idnumber'] = 'TEST1';
         $group2 = array();
         $group2['courseid'] = $course->id;
         $group2['name'] = 'Group Test 2';
@@ -58,6 +59,11 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
         $group3['courseid'] = $course->id;
         $group3['name'] = 'Group Test 3';
         $group3['description'] = 'Group Test 3 description';
+        $group3['idnumber'] = 'TEST1';
+        $group4 = array();
+        $group4['courseid'] = $course->id;
+        $group4['name'] = 'Group Test 4';
+        $group4['description'] = 'Group Test 4 description';
 
         // Set the required capabilities by the external function
         $context = context_course::instance($course->id);
@@ -80,6 +86,7 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
                     $groupcourseid = $group1['courseid'];
                     $this->assertEquals($dbgroup->descriptionformat, $group1['descriptionformat']);
                     $this->assertEquals($dbgroup->enrolmentkey, $group1['enrolmentkey']);
+                    $this->assertEquals($dbgroup->idnumber, $group1['idnumber']);
                     break;
                 case $group2['name']:
                     $groupdescription = $group2['description'];
@@ -93,10 +100,19 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
             $this->assertEquals($dbgroup->courseid, $groupcourseid);
         }
 
+        try {
+            $froups = core_group_external::create_groups(array($group3));
+            $this->fail('Exception expected due to already existing idnumber.');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('invalid_parameter_exception', $e);
+            $this->assertEquals('Invalid parameter value detected (Group with the same idnumber already exists)',
+                $e->getMessage());
+        }
+
         // Call without required capability
         $this->unassignUserCapability('moodle/course:managegroups', $context->id, $roleid);
         $this->setExpectedException('required_capability_exception');
-        $froups = core_group_external::create_groups(array($group3));
+        $froups = core_group_external::create_groups(array($group4));
     }
 
     /**
@@ -114,6 +130,7 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
         $group1data['description'] = 'Group Test 1 description';
         $group1data['descriptionformat'] = FORMAT_MOODLE;
         $group1data['enrolmentkey'] = 'Test group enrol secret phrase';
+        $group1data['idnumber'] = 'TEST1';
         $group2data = array();
         $group2data['courseid'] = $course->id;
         $group2data['name'] = 'Group Test 2';
@@ -142,6 +159,7 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
                     $groupcourseid = $group1->courseid;
                     $this->assertEquals($dbgroup->descriptionformat, $group1->descriptionformat);
                     $this->assertEquals($dbgroup->enrolmentkey, $group1->enrolmentkey);
+                    $this->assertEquals($dbgroup->idnumber, $group1->idnumber);
                     break;
                 case $group2->name:
                     $groupdescription = $group2->description;
@@ -209,6 +227,67 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
         $froups = core_group_external::delete_groups(array($group3->id));
     }
 
+    /**
+     * Test create and update groupings.
+     * @return void
+     */
+    public function test_create_update_groupings() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $this->setAdminUser();
+
+        $course = self::getDataGenerator()->create_course();
+
+        $grouping1data = array();
+        $grouping1data['courseid'] = $course->id;
+        $grouping1data['name'] = 'Grouping 1 Test';
+        $grouping1data['description'] = 'Grouping 1 Test description';
+        $grouping1data['descriptionformat'] = FORMAT_MOODLE;
+        $grouping1data['idnumber'] = 'TEST';
+
+        $grouping1 = self::getDataGenerator()->create_grouping($grouping1data);
+
+        $grouping1data['name'] = 'Another group';
+
+        try {
+            $groupings = core_group_external::create_groupings(array($grouping1data));
+            $this->fail('Exception expected due to already existing idnumber.');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('invalid_parameter_exception', $e);
+            $this->assertEquals('Invalid parameter value detected (Grouping with the same idnumber already exists)',
+                $e->getMessage());
+        }
+
+        // No exception should be triggered.
+        $grouping1data['id'] = $grouping1->id;
+        $grouping1data['idnumber'] = 'CHANGED';
+        unset($grouping1data['courseid']);
+        core_group_external::update_groupings(array($grouping1data));
+
+        $grouping2data = array();
+        $grouping2data['courseid'] = $course->id;
+        $grouping2data['name'] = 'Grouping 2 Test';
+        $grouping2data['description'] = 'Grouping 2 Test description';
+        $grouping2data['descriptionformat'] = FORMAT_MOODLE;
+        $grouping2data['idnumber'] = 'TEST';
+
+        $grouping2 = self::getDataGenerator()->create_grouping($grouping2data);
+
+        $grouping2data['id'] = $grouping2->id;
+        $grouping2data['idnumber'] = 'CHANGED';
+        unset($grouping2data['courseid']);
+        try {
+            $groupings = core_group_external::update_groupings(array($grouping2data));
+            $this->fail('Exception expected due to already existing idnumber.');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('invalid_parameter_exception', $e);
+            $this->assertEquals('Invalid parameter value detected (A different grouping with the same idnumber already exists)',
+                $e->getMessage());
+        }
+    }
+
     /**
      * Test get_groupings
      */
@@ -313,6 +392,7 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
         $group1data['courseid'] = $course->id;
         $group1data['name'] = 'Group Test 1';
         $group1data['description'] = 'Group Test 1 description';
+        $group1data['idnumber'] = 'TEST1';
         $group2data = array();
         $group2data['courseid'] = $course->id;
         $group2data['name'] = 'Group Test 2';
@@ -339,6 +419,7 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
 
         $this->assertEquals($group1data['name'], $groups['groups'][0]['name']);
         $this->assertEquals($group1data['description'], $groups['groups'][0]['description']);
+        $this->assertEquals($group1data['idnumber'], $groups['groups'][0]['idnumber']);
 
         $this->setUser($teacher);
         $groups = core_group_external::get_course_user_groups($course->id, $student1->id);
@@ -372,5 +453,4 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
         $this->assertCount(1, $groups['warnings']);
 
     }
-
 }
index 7170e30..2a264c5 100644 (file)
@@ -31,7 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['admindirname'] = 'Directorio Admin';
-$string['availablelangs'] = 'Lista de idiomas disponibles';
+$string['availablelangs'] = 'Packs de idiomas disponibles';
 $string['chooselanguagehead'] = 'Seleccionar idioma';
 $string['chooselanguagesub'] = 'Por favor, seleccione un idioma para el proceso de instalación. Este idioma se usará también como idioma por defecto del sitio, si bien puede cambiarse más adelante.';
 $string['clialreadyconfigured'] = 'El archivo de configuración config.php ya existe. Por favor, utilice admin/cli/install_database.php para instalar Moodle en este sitio.';
@@ -74,7 +74,7 @@ $string['pathsroparentdataroot'] = 'El directorio padre ({$a->parent}) no tiene
 $string['pathssubadmindir'] = 'Muy pocos servidores web usan /admin como un URL especial para acceder a un
 panel de control o algo similar. Lamentablemente, esto entra en conflicto con la ubicación estándar para las páginas de administración de Moodle. Usted puede solucionar este problema, renombrando el directorio admin en su instalación Moodle, poniendo un nuevo nombre aquí. Por ejemplo: <em> moodleadmin </em>. Esto solucionará los enlaces de administración en instalación Moodle.';
 $string['pathssubdataroot'] = 'Usted necesita un espacio donde Moodle puede guardar los archivos subidos. En este directorio debe poder LEER y ESCRIBIR el usuario del servidor web (por lo general \'nobody\',  \'apache\' o \'www-data\'), pero no debe poderse acceder a esta carpeta directamente a través de la web. El instalador tratará de crearla si no existe.';
-$string['pathssubdirroot'] = 'Ruta completa del directorio de instalación de Moodle.';
+$string['pathssubdirroot'] = '<p>Ruta completa del directorio que contiene el código de  Moodle.</p>';
 $string['pathssubwwwroot'] = 'Dirección web completa para acceder a Moodle. No es posible acceder a Moodle utilizando múltiples direcciones. Si su sitio tiene varias direcciones públicas debe configurar redirecciones permanentes en todas ellas, excepto en ésta. Si su sitio web es accesible tanto desde una intranet como desde Internet, escriba aquí la dirección pública y configure su DNS para que los usuarios de su intranet puedan también utilizar la dirección pública.';
 $string['pathsunsecuredataroot'] = 'La ubicación de dataroot no es segura';
 $string['pathswrongadmindir'] = 'El directorio admin no existe';
index 58421d5..c0bc147 100644 (file)
@@ -34,6 +34,9 @@ $string['admindirname'] = 'Директориум аdmin';
 $string['availablelangs'] = 'Достапни јазични пакети';
 $string['chooselanguagehead'] = 'Изберете јазик';
 $string['chooselanguagesub'] = 'Изберете јазик САМО за инсталацијата. Подоцна ќе можете да изберете јазик за страницата и за корисниците.';
+$string['clialreadyconfigured'] = 'Конфигурациската датотека config.php веќе постои. Ве молиме користете ја admin/cli/install_database.php за да го инсталирате Moodle на овој сајт.';
+$string['clialreadyinstalled'] = 'Конфигурациската датотека config.php веќе постои. Ве молиме користете ја admin/cli/install_database.php за да го надградите Moodle за овој сајт.';
+$string['cliinstallheader'] = 'Програма за инсталирање на Moodle {$a} командна линија';
 $string['dataroot'] = 'Директориум';
 $string['dbprefix'] = 'Префикс на табели';
 $string['dirroot'] = 'Moodle директориум';
index b0498eb..1ca0a6e 100644 (file)
@@ -94,6 +94,7 @@ $string['manageinstance'] = 'Manage';
 $string['migratetomanual'] = 'Migrate to manual enrolments';
 $string['nochange'] = 'No change';
 $string['noexistingparticipants'] = 'No existing participants';
+$string['nogroup'] = 'No group';
 $string['noguestaccess'] = 'Guests can not access this course, please try to log in.';
 $string['none'] = 'None';
 $string['notenrollable'] = 'You can not enrol yourself in this course.';
index b062892..e7fed94 100644 (file)
@@ -239,6 +239,9 @@ $string['gradeadministration'] = 'Grade administration';
 $string['gradealreadyupdated'] = '{$a} grades have not been imported because the grades in the import file are older than in the grader report. To proceed with the grade import anyway, use the force import option.';
 $string['gradeanalysis'] = 'Grade analysis';
 $string['gradebook'] = 'Gradebook';
+$string['gradebookcalculationsuptodate'] = 'The calculations in the gradebook are up to date. You may need to reload this page to see changes.';
+$string['gradebookcalculationsfixbutton'] = 'Accept grade changes and fix calculation errors';
+$string['gradebookcalculationswarning'] = 'Note: Some errors have been detected in calculating the grades displayed in the gradebook. It is recommended that the errors are fixed by clicking the button below, though this will result in some grades being changed. For details, see the changes between versions {$a->gradebookversion} and {$a->currentversion} in <a href="{$a->url}">Gradebook calculation changes</a>.';
 $string['gradebookhiddenerror'] = 'The gradebook is currently set to hide everything from students.';
 $string['gradebookhistories'] = 'Grade histories';
 $string['gradeboundary'] = 'Letter grade boundary';
index 8e1ea6b..701eb03 100644 (file)
@@ -107,6 +107,8 @@ $string['instanceismisconfigured'] = 'Portfolio instance is misconfigured, skipp
 $string['instancenotdelete'] = 'Failed to delete portfolio';
 $string['instancenotsaved'] = 'Failed to save portfolio';
 $string['instancesaved'] = 'Portfolio saved successfully';
+$string['intro'] = 'Content which you have created, such as assignment submissions, forum posts and blog entries, can be exported to a portfolio or downloaded.<br>
+Any portfolio that you do not wish to use may be hidden so that it is not listed as an option to export content to.';
 $string['invalidaddformat'] = 'Invalid add format passed to portfolio_add_button. ({$a}) Must be one of PORTFOLIO_ADD_XXX';
 $string['invalidbuttonproperty'] = 'Could not find that property ({$a}) of portfolio_button';
 $string['invalidconfigproperty'] = 'Could not find that config property ({$a->property} of {$a->class})';
@@ -170,6 +172,7 @@ $string['save'] = 'Save';
 $string['selectedformat'] = 'Selected export format';
 $string['selectedwait'] = 'Selected to wait?';
 $string['selectplugin'] = 'Select destination';
+$string['showhide'] = 'Show / hide';
 $string['singleinstancenomultiallowed'] = 'Only a single portfolio plugin instance is available, it doesn\'t support multiple exports per session, and there\'s already an active export in the session using this plugin!';
 $string['somepluginsdisabled'] = 'Some entire portfolio plugins have been disabled because they are either misconfigured or rely on something else that is:';
 $string['sure'] = 'Are you sure you want to delete \'{$a}\'? This cannot be undone.';
index ef27f56..e9360f0 100644 (file)
@@ -4724,6 +4724,50 @@ class admin_setting_pickroles extends admin_setting_configmulticheckbox {
 }
 
 
+/**
+ * Admin setting that is a list of installed filter plugins.
+ *
+ * @copyright 2015 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class admin_setting_pickfilters extends admin_setting_configmulticheckbox {
+
+    /**
+     * Constructor
+     *
+     * @param string $name unique ascii name, either 'mysetting' for settings
+     *      that in config, or 'myplugin/mysetting' for ones in config_plugins.
+     * @param string $visiblename localised name
+     * @param string $description localised long description
+     * @param array $default the default. E.g. array('urltolink' => 1, 'emoticons' => 1)
+     */
+    public function __construct($name, $visiblename, $description, $default) {
+        if (empty($default)) {
+            $default = array();
+        }
+        $this->load_choices();
+        foreach ($default as $plugin) {
+            if (!isset($this->choices[$plugin])) {
+                unset($default[$plugin]);
+            }
+        }
+        parent::__construct($name, $visiblename, $description, $default, null);
+    }
+
+    public function load_choices() {
+        if (is_array($this->choices)) {
+            return true;
+        }
+        $this->choices = array();
+
+        foreach (core_component::get_plugin_list('filter') as $plugin => $unused) {
+            $this->choices[$plugin] = filter_get_name($plugin);
+        }
+        return true;
+    }
+}
+
+
 /**
  * Text field with an advanced checkbox, that controls a additional $name.'_adv' setting.
  *
index 9c0d683..ed309f0 100644 (file)
@@ -157,7 +157,7 @@ class behat_command {
 
             // Returning composer error code to avoid conflicts with behat and moodle error codes.
             self::output_msg(get_string('errorcomposer', 'tool_behat'));
-            return BEHAT_EXITCODE_COMPOSER;
+            return TESTING_EXITCODE_COMPOSER;
         }
 
         // Behat test command.
@@ -167,7 +167,7 @@ class behat_command {
 
             // Returning composer error code to avoid conflicts with behat and moodle error codes.
             self::output_msg(get_string('errorbehatcommand', 'tool_behat', self::get_behat_command()));
-            return BEHAT_EXITCODE_COMPOSER;
+            return TESTING_EXITCODE_COMPOSER;
         }
 
         // No empty values.
index 7a422ed..d80486e 100644 (file)
@@ -38,7 +38,6 @@ define('BEHAT_EXITCODE_REQUIREMENT', 251);
 define('BEHAT_EXITCODE_PERMISSIONS', 252);
 define('BEHAT_EXITCODE_REINSTALL', 253);
 define('BEHAT_EXITCODE_INSTALL', 254);
-define('BEHAT_EXITCODE_COMPOSER', 255);
 define('BEHAT_EXITCODE_INSTALLED', 256);
 
 /**
@@ -458,4 +457,4 @@ function cli_execute_parallel($cmds, $cwd = null) {
         }
     }
     return $processes;
-}
\ No newline at end of file
+}
index 393d0e7..f75cce9 100644 (file)
@@ -215,6 +215,20 @@ class message {
         throw new \coding_exception("Invalid property $prop specified");
     }
 
+    /**
+     * Magic method to check if property is set.
+     *
+     * @param string $prop name of property to check.
+     * @return bool
+     * @throws \coding_exception
+     */
+    public function __isset($prop) {
+        if (in_array($prop, $this->properties)) {
+            return isset($this->$prop);
+        }
+        throw new \coding_exception("Invalid property $prop specified");
+    }
+
     /**
      * This method lets you define content that would be added to the message only for specific message processors.
      *
index be869e3..962a700 100644 (file)
@@ -176,3 +176,35 @@ function cli_error($text, $errorcode=1) {
     die($errorcode);
 }
 
+/**
+ * Print an ASCII version of the Moodle logo.
+ *
+ * @param int $padding left padding of the logo
+ * @param bool $return should we print directly (false) or return the string (true)
+ * @return mixed void or string
+ */
+function cli_logo($padding=2, $return=false) {
+
+    $lines = array(
+        '                               .-..-.       ',
+        ' _____                         | || |       ',
+        '/____/-.---_  .---.  .---.  .-.| || | .---. ',
+        '| |  _   _  |/  _  \\/  _  \\/  _  || |/  __ \\',
+        '* | | | | | || |_| || |_| || |_| || || |___/',
+        '  |_| |_| |_|\\_____/\\_____/\\_____||_|\\_____)',
+    );
+
+    $logo = '';
+
+    foreach ($lines as $line) {
+        $logo .= str_repeat(' ', $padding);
+        $logo .= $line;
+        $logo .= PHP_EOL;
+    }
+
+    if ($return) {
+        return $logo;
+    } else {
+        echo $logo;
+    }
+}
index 0d8e132..d3ca874 100644 (file)
@@ -131,6 +131,8 @@ function xmldb_main_install() {
         'filterall'             => 0, // setting page, so have to be initialised here.
         'texteditors'           => 'atto,tinymce,textarea',
         'upgrade_minmaxgradestepignored' => 1, // New installs should not run this upgrade step.
+        'upgrade_extracreditweightsstepignored' => 1, // New installs should not run this upgrade step.
+        'upgrade_calculatedgradeitemsignored' => 1, // New installs should not run this upgrade step.
     );
     foreach($defaults as $key => $value) {
         set_config($key, $value);
index fe2f44a..10992e8 100644 (file)
@@ -4061,6 +4061,8 @@ function xmldb_main_upgrade($oldversion) {
     if ($oldversion < 2014111000.00) {
         // Coming from 2.7 or older, we need to flag the step minmaxgrade to be ignored.
         set_config('upgrade_minmaxgradestepignored', 1);
+        // Coming from 2.7 or older, we need to flag the step for changing calculated grades to be regraded.
+        set_config('upgrade_calculatedgradeitemsonlyregrade', 1);
 
         // Main savepoint reached.
         upgrade_main_savepoint(true, 2014111000.00);
@@ -4399,5 +4401,45 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2015060400.02);
     }
 
+    if ($oldversion < 2015061900.00) {
+        // MDL-49257. Changed the algorithm of calculating automatic weights of extra credit items.
+
+        // Before the change, in case when grade category (in "Natural" agg. method) had items with
+        // overridden weights, the automatic weight of extra credit items was illogical.
+        // In order to prevent grades changes after the upgrade we need to freeze gradebook calculation
+        // for the affected courses.
+
+        // This script in included in each major version upgrade process so make sure we don't run it twice.
+        if (empty($CFG->upgrade_extracreditweightsstepignored)) {
+            upgrade_extra_credit_weightoverride();
+
+            // To skip running the same script on the upgrade to the next major release.
+            set_config('upgrade_extracreditweightsstepignored', 1);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2015061900.00);
+    }
+
+    if ($oldversion < 2015062500.01) {
+        // MDL-48239. Changed calculated grade items so that the maximum and minimum grade can be set.
+
+        // If the changes are accepted and a regrade is done on the gradebook then some grades may change significantly.
+        // This is here to freeze the gradebook in affected courses.
+
+        // This script is included in each major version upgrade process so make sure we don't run it twice.
+        if (empty($CFG->upgrade_calculatedgradeitemsignored)) {
+            upgrade_calculated_grade_items();
+
+            // To skip running the same script on the upgrade to the next major release.
+            set_config('upgrade_calculatedgradeitemsignored', 1);
+            // This config value is never used again.
+            unset_config('upgrade_calculatedgradeitemsonlyregrade');
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2015062500.01);
+    }
+
     return true;
 }
index 81b92b5..2c901e6 100644 (file)
@@ -529,4 +529,142 @@ function upgrade_mimetypes($filetypes) {
             array($extension)
         );
     }
+}
+
+/**
+ * Marks all courses with changes in extra credit weight calculation
+ *
+ * Used during upgrade and in course restore process
+ *
+ * This upgrade script is needed because we changed the algorithm for calculating the automatic weights of extra
+ * credit items and want to prevent changes in the existing student grades.
+ *
+ * @param int $onlycourseid
+ */
+function upgrade_extra_credit_weightoverride($onlycourseid = 0) {
+    global $DB;
+
+    // Find all courses that have categories in Natural aggregation method where there is at least one extra credit
+    // item and at least one item with overridden weight.
+    $courses = $DB->get_fieldset_sql(
+        "SELECT DISTINCT gc.courseid
+          FROM {grade_categories} gc
+          INNER JOIN {grade_items} gi ON gc.id = gi.categoryid AND gi.weightoverride = :weightoverriden
+          INNER JOIN {grade_items} gie ON gc.id = gie.categoryid AND gie.aggregationcoef = :extracredit
+          WHERE gc.aggregation = :naturalaggmethod" . ($onlycourseid ? " AND gc.courseid = :onlycourseid" : ''),
+        array('naturalaggmethod' => 13,
+            'weightoverriden' => 1,
+            'extracredit' => 1,
+            'onlycourseid' => $onlycourseid,
+        )
+    );
+    foreach ($courses as $courseid) {
+        $gradebookfreeze = get_config('core', 'gradebook_calculations_freeze_' . $courseid);
+        if (!$gradebookfreeze) {
+            set_config('gradebook_calculations_freeze_' . $courseid, 20150619);
+        }
+    }
+}
+
+/**
+ * Marks all courses that require calculated grade items be updated.
+ *
+ * Used during upgrade and in course restore process.
+ *
+ * This upgrade script is needed because the calculated grade items were stuck with a maximum of 100 and could be changed.
+ * This flags the courses that are affected and the grade book is frozen to retain grade integrity.
+ *
+ * @param int $courseid Specify a course ID to run this script on just one course.
+ */
+function upgrade_calculated_grade_items($courseid = null) {
+    global $DB, $CFG;
+
+    $affectedcourses = array();
+    $possiblecourseids = array();
+    $params = array();
+    $singlecoursesql = '';
+    if (isset($courseid)) {
+        $singlecoursesql = "AND ns.id = :courseid";
+        $params['courseid'] = $courseid;
+    }
+    $siteminmaxtouse = 1;
+    if (isset($CFG->grade_minmaxtouse)) {
+        $siteminmaxtouse = $CFG->grade_minmaxtouse;
+    }
+    $courseidsql = "SELECT ns.id
+                      FROM (
+                        SELECT c.id, coalesce(" . $DB->sql_compare_text('gs.value') . ", :siteminmax) AS gradevalue
+                          FROM {course} c
+                          LEFT JOIN {grade_settings} gs
+                            ON c.id = gs.courseid
+                           AND ((gs.name = 'minmaxtouse' AND " . $DB->sql_compare_text('gs.value') . " = '2'))
+                        ) ns
+                    WHERE " . $DB->sql_compare_text('ns.gradevalue') . " = '2' $singlecoursesql";
+    $params['siteminmax'] = $siteminmaxtouse;
+    $courses = $DB->get_records_sql($courseidsql, $params);
+    foreach ($courses as $course) {
+        $possiblecourseids[$course->id] = $course->id;
+    }
+
+    if (!empty($possiblecourseids)) {
+        list($sql, $params) = $DB->get_in_or_equal($possiblecourseids);
+        // A calculated grade item grade min != 0 and grade max != 100 and the course setting is set to
+        // "Initial min and max grades".
+        $coursesql = "SELECT DISTINCT courseid
+                        FROM {grade_items}
+                       WHERE calculation IS NOT NULL
+                         AND itemtype = 'manual'
+                         AND (grademax <> 100 OR grademin <> 0)
+                         AND courseid $sql";
+        $affectedcourses = $DB->get_records_sql($coursesql, $params);
+    }
+
+    // Check for second type of affected courses.
+    // If we already have the courseid parameter set in the affectedcourses then there is no need to run through this section.
+    if (!isset($courseid) || !in_array($courseid, $affectedcourses)) {
+        $singlecoursesql = '';
+        $params = array();
+        if (isset($courseid)) {
+            $singlecoursesql = "AND courseid = :courseid";
+            $params['courseid'] = $courseid;
+        }
+        $nestedsql = "SELECT id
+                        FROM {grade_items}
+                       WHERE itemtype = 'category'
+                         AND calculation IS NOT NULL $singlecoursesql";
+        $calculatedgradecategories = $DB->get_records_sql($nestedsql, $params);
+        $categoryids = array();
+        foreach ($calculatedgradecategories as $key => $gradecategory) {
+            $categoryids[$key] = $gradecategory->id;
+        }
+
+        if (!empty($categoryids)) {
+            list($sql, $params) = $DB->get_in_or_equal($categoryids);
+            // A category with a calculation where the raw grade min and the raw grade max don't match the grade min and grade max
+            // for the category.
+            $coursesql = "SELECT DISTINCT gi.courseid
+                            FROM {grade_grades} gg, {grade_items} gi
+                           WHERE gi.id = gg.itemid
+                             AND (gg.rawgrademax <> gi.grademax OR gg.rawgrademin <> gi.grademin)
+                             AND gi.id $sql";
+            $additionalcourses = $DB->get_records_sql($coursesql, $params);
+            foreach ($additionalcourses as $key => $additionalcourse) {
+                if (!array_key_exists($key, $affectedcourses)) {
+                    $affectedcourses[$key] = $additionalcourse;
+                }
+            }
+        }
+    }
+
+    foreach ($affectedcourses as $affectedcourseid) {
+        if (isset($CFG->upgrade_calculatedgradeitemsonlyregrade) && !($courseid)) {
+            $DB->set_field('grade_items', 'needsupdate', 1, array('courseid' => $affectedcourseid->courseid));
+        } else {
+            // Check to see if the gradebook freeze is already in affect.
+            $gradebookfreeze = get_config('core', 'gradebook_calculations_freeze_' . $affectedcourseid->courseid);
+            if (!$gradebookfreeze) {
+                set_config('gradebook_calculations_freeze_' . $affectedcourseid->courseid, 20150627);
+            }
+        }
+    }
 }
\ No newline at end of file
index 185bcd6..6375faf 100644 (file)
@@ -2684,6 +2684,7 @@ class curl {
 
     /** @var array cURL options */
     private $options;
+
     /** @var string Proxy host */
     private $proxy_host = '';
     /** @var string Proxy auth */
@@ -3004,15 +3005,36 @@ class curl {
         }
 
         $this->setopt($options);
-        // reset before set options
+
+        // Reset before set options.
         curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this,'formatHeader'));
-        // set headers
+
+        // Setting the User-Agent based on options provided.
+        $useragent = '';
+
+        if (!empty($options['CURLOPT_USERAGENT'])) {
+            $useragent = $options['CURLOPT_USERAGENT'];
+        } else if (!empty($this->options['CURLOPT_USERAGENT'])) {
+            $useragent = $this->options['CURLOPT_USERAGENT'];
+        } else {
+            $useragent = 'MoodleBot/1.0';
+        }
+
+        // Set headers.
         if (empty($this->header)) {
             $this->setHeader(array(
-                'User-Agent: MoodleBot/1.0',
+                'User-Agent: ' . $useragent,
                 'Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7',
                 'Connection: keep-alive'
                 ));
+        } else if (!in_array('User-Agent: ' . $useragent, $this->header)) {
+            // Remove old User-Agent if one existed.
+            // We have to partial search since we don't know what the original User-Agent is.
+            if ($match = preg_grep('/User-Agent.*/', $this->header)) {
+                $key = array_keys($match)[0];
+                unset($this->header[$key]);
+            }
+            $this->setHeader(array('User-Agent: ' . $useragent));
         }
         curl_setopt($curl, CURLOPT_HTTPHEADER, $this->header);
 
index f47d6ce..ba89fd2 100644 (file)
@@ -17,9 +17,9 @@
 /**
  * Library functions for managing text filter plugins.
  *
- * @package    core_filter
- * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package   core
+ * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
@@ -48,34 +48,38 @@ define('TEXTFILTER_EXCL_SEPARATOR', '-%-');
  *
  * This class is a singleton.
  *
- * @package    core_filter
  * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class filter_manager {
     /**
-     * @var array This list of active filters, by context, for filtering content.
-     * An array contextid => array of filter objects.
+     * @var moodle_text_filter[][] This list of active filters, by context, for filtering content.
+     * An array contextid => ordered array of filter name => filter objects.
      */
     protected $textfilters = array();
 
     /**
-     * @var array This list of active filters, by context, for filtering strings.
-     * An array contextid => array of filter objects.
+     * @var moodle_text_filter[][] This list of active filters, by context, for filtering strings.
+     * An array contextid => ordered array of filter name => filter objects.
      */
     protected $stringfilters = array();
 
     /** @var array Exploded version of $CFG->stringfilters. */
     protected $stringfilternames = array();
 
-    /** @var object Holds the singleton instance. */
+    /** @var filter_manager Holds the singleton instance. */
     protected static $singletoninstance;
 
+    /**
+     * Constructor. Protected. Use {@link instance()} instead.
+     */
     protected function __construct() {
         $this->stringfilternames = filter_get_string_filters();
     }
 
     /**
+     * Factory method. Use this to get the filter manager.
+     *
      * @return filter_manager the singleton instance.
      */
     public static function instance() {
@@ -112,7 +116,7 @@ class filter_manager {
     /**
      * Load all the filters required by this context.
      *
-     * @param object $context
+     * @param context $context the context.
      */
     protected function load_filters($context) {
         $filters = filter_get_active_in_context($context);
@@ -123,9 +127,9 @@ class filter_manager {
             if (is_null($filter)) {
                 continue;
             }
-            $this->textfilters[$context->id][] = $filter;
+            $this->textfilters[$context->id][$filtername] = $filter;
             if (in_array($filtername, $this->stringfilternames)) {
-                $this->stringfilters[$context->id][] = $filter;
+                $this->stringfilters[$context->id][$filtername] = $filter;
             }
         }
     }
@@ -156,23 +160,29 @@ class filter_manager {
     }
 
     /**
-     * @todo Document this function
+     * Apply a list of filters to some content.
      * @param string $text
-     * @param array $filterchain
-     * @param array $options options passed to the filters
+     * @param moodle_text_filter[] $filterchain array filter name => filter object.
+     * @param array $options options passed to the filters.
+     * @param array $skipfilters of filter names. Any filters that should not be applied to this text.
      * @return string $text
      */
-    protected function apply_filter_chain($text, $filterchain, array $options = array()) {
-        foreach ($filterchain as $filter) {
+    protected function apply_filter_chain($text, $filterchain, array $options = array(),
+            array $skipfilters = null) {
+        foreach ($filterchain as $filtername => $filter) {
+            if ($skipfilters !== null && in_array($filtername, $skipfilters)) {
+                continue;
+            }
             $text = $filter->filter($text, $options);
         }
         return $text;
     }
 
     /**
-     * @todo Document this function
-     * @param object $context
-     * @return object A text filter
+     * Get all the filters that apply to a given context for calls to format_text.
+     *
+     * @param context $context
+     * @return moodle_text_filter[] A text filter
      */
     protected function get_text_filters($context) {
         if (!isset($this->textfilters[$context->id])) {
@@ -182,9 +192,10 @@ class filter_manager {
     }
 
     /**
-     * @todo Document this function
-     * @param object $context
-     * @return object A string filter
+     * Get all the filters that apply to a given context for calls to format_string.
+     *
+     * @param context $context the context.
+     * @return moodle_text_filter[] A text filter
      */
     protected function get_string_filters($context) {
         if (!isset($this->stringfilters[$context->id])) {
@@ -197,12 +208,14 @@ class filter_manager {
      * Filter some text
      *
      * @param string $text The text to filter
-     * @param object $context
+     * @param context $context the context.
      * @param array $options options passed to the filters
+     * @param array $skipfilters of filter names. Any filters that should not be applied to this text.
      * @return string resulting text
      */
-    public function filter_text($text, $context, array $options = array()) {
-        $text = $this->apply_filter_chain($text, $this->get_text_filters($context), $options);
+    public function filter_text($text, $context, array $options = array(),
+            array $skipfilters = null) {
+        $text = $this->apply_filter_chain($text, $this->get_text_filters($context), $options, $skipfilters);
         // <nolink> tags removed for XHTML compatibility
         $text = str_replace(array('<nolink>', '</nolink>'), '', $text);
         return $text;
@@ -212,7 +225,7 @@ class filter_manager {
      * Filter a piece of string
      *
      * @param string $string The text to filter
-     * @param context $context
+     * @param context $context the context.
      * @return string resulting string
      */
     public function filter_string($string, $context) {
@@ -220,11 +233,15 @@ class filter_manager {
     }
 
     /**
-     * @todo Document this function
-     * @param context $context
-     * @return object A string filter
+     * @deprecated Since Moodle 3.0 MDL-50491. This was used by the old text filtering system, but no more.
+     * @todo MDL-50632 This will be deleted in Moodle 3.2.
+     * @param context $context the context.
+     * @return string the hash.
      */
     public function text_filtering_hash($context) {
+        debugging('filter_manager::text_filtering_hash() is deprecated. ' .
+                'It was an internal part of the old format_text caching, ' .
+                'and should not have been called from other code.', DEBUG_DEVELOPER);
         $filters = $this->get_text_filters($context);
         $hashes = array();
         foreach ($filters as $filter) {
@@ -257,57 +274,49 @@ class filter_manager {
     }
 }
 
+
 /**
  * Filter manager subclass that does nothing. Having this simplifies the logic
  * of format_text, etc.
  *
- * @todo Document this class
- *
- * @package    core_filter
- * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class null_filter_manager {
-    /**
-     * @return string
-     */
-    public function filter_text($text, $context, $options) {
+    public function filter_text($text, $context, array $options = array(),
+            array $skipfilters = null) {
         return $text;
     }
 
-    /**
-     * @return string
-     */
     public function filter_string($string, $context) {
         return $string;
     }
 
-    /**
-     * @return string
-     */
     public function text_filtering_hash() {
+        debugging('filter_manager::text_filtering_hash() is deprecated. ' .
+                'It was an internal part of the old format_text caching, ' .
+                'and should not have been called from other code.', DEBUG_DEVELOPER);
         return '';
     }
 }
 
+
 /**
- * Filter manager subclass that tacks how much work it does.
+ * Filter manager subclass that tracks how much work it does.
  *
- * @todo Document this class
- *
- * @package    core_filter
- * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class performance_measuring_filter_manager extends filter_manager {
-    /** @var int */
+    /** @var int number of filter objects created. */
     protected $filterscreated = 0;
+
+    /** @var int number of calls to filter_text. */
     protected $textsfiltered = 0;
+
+    /** @var int number of calls to filter_string. */
     protected $stringsfiltered = 0;
 
-    /**
-     * Unloads all filters and other cached information
-     */
     protected function unload_all_filters() {
         parent::unload_all_filters();
         $this->filterscreated = 0;
@@ -315,40 +324,25 @@ class performance_measuring_filter_manager extends filter_manager {
         $this->stringsfiltered = 0;
     }
 
-    /**
-     * @param string $filtername
-     * @param object $context
-     * @param mixed $localconfig
-     * @return mixed
-     */
     protected function make_filter_object($filtername, $context, $localconfig) {
         $this->filterscreated++;
         return parent::make_filter_object($filtername, $context, $localconfig);
     }
 
-    /**
-     * @param string $text
-     * @param object $context
-     * @param array $options options passed to the filters
-     * @return mixed
-     */
-    public function filter_text($text, $context, array $options = array()) {
+    public function filter_text($text, $context, array $options = array(),
+            array $skipfilters = null) {
         $this->textsfiltered++;
-        return parent::filter_text($text, $context, $options);
+        return parent::filter_text($text, $context, $options, $skipfilters);
     }
 
-    /**
-     * @param string $string
-     * @param object $context
-     * @return mixed
-     */
     public function filter_string($string, $context) {
         $this->stringsfiltered++;
         return parent::filter_string($string, $context);
     }
 
     /**
-     * @return array
+     * Return performance information, in the form required by {@link get_performance_info()}.
+     * @return array the performance info.
      */
     public function get_performance_summary() {
         return array(array(
@@ -365,17 +359,18 @@ class performance_measuring_filter_manager extends filter_manager {
     }
 }
 
+
 /**
  * Base class for text filters. You just need to override this class and
  * implement the filter method.
  *
- * @package    core_filter
- * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 abstract class moodle_text_filter {
-    /** @var object The context we are in. */
+    /** @var context The context we are in. */
     protected $context;
+
     /** @var array Any local configuration for this filter in this context. */
     protected $localconfig;
 
@@ -391,9 +386,14 @@ abstract class moodle_text_filter {
     }
 
     /**
+     * @deprecated Since Moodle 3.0 MDL-50491. This was used by the old text filtering system, but no more.
+     * @todo MDL-50632 This will be deleted in Moodle 3.2.
      * @return string The class name of the current class
      */
     public function hash() {
+        debugging('moodle_text_filter::hash() is deprecated. ' .
+                'It was an internal part of the old format_text caching, ' .
+                'and should not have been called from other code.', DEBUG_DEVELOPER);
         return __CLASS__;
     }
 
@@ -425,15 +425,14 @@ abstract class moodle_text_filter {
     public abstract function filter($text, array $options = array());
 }
 
+
 /**
  * This is just a little object to define a phrase and some instructions
  * for how to process it.  Filters can create an array of these to pass
  * to the filter_phrases function below.
  *
- * @package    core
- * @subpackage filter
- * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  **/
 class filterobject {
     /** @var string */
index 3830b7a..fc80f24 100644 (file)
@@ -1166,6 +1166,12 @@ class grade_category extends grade_object {
                 $this->load_grade_item();
                 $num = count($grade_values);
                 $sum = 0;
+
+                // This setting indicates if we should use algorithm prior to MDL-49257 fix for calculating extra credit weights.
+                // Even though old algorith has bugs in it, we need to preserve existing grades.
+                $gradebookcalculationfreeze = (int)get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
+                $oldextracreditcalculation = $gradebookcalculationfreeze && ($gradebookcalculationfreeze <= 20150619);
+
                 $sumweights = 0;
                 $grademin = 0;
                 $grademax = 0;
@@ -1205,7 +1211,11 @@ class grade_category extends grade_object {
                             $userweights[$itemid] = 0;
                             continue;
                         }
-                        $userweights[$itemid] = $items[$itemid]->aggregationcoef2 / $sumweights;
+                        $userweights[$itemid] = $sumweights ? ($items[$itemid]->aggregationcoef2 / $sumweights) : 0;
+                        if (!$oldextracreditcalculation && isset($extracredititems[$itemid])) {
+                            // Extra credit items do not affect totals.
+                            continue;
+                        }
                         $totaloverriddenweight += $userweights[$itemid];
                         $usergrademax = $items[$itemid]->grademax;
                         if (isset($grademaxoverrides[$itemid])) {
@@ -1216,9 +1226,9 @@ class grade_category extends grade_object {
                 }
                 $nonoverriddenpoints = $grademax - $totaloverriddengrademax;
 
-                // Then we need to recalculate the automatic weights.
+                // Then we need to recalculate the automatic weights except for extra credit items.
                 foreach ($grade_values as $itemid => $gradevalue) {
-                    if (!$items[$itemid]->weightoverride) {
+                    if (!$items[$itemid]->weightoverride && ($oldextracreditcalculation || !isset($extracredititems[$itemid]))) {
                         $usergrademax = $items[$itemid]->grademax;
                         if (isset($grademaxoverrides[$itemid])) {
                             $usergrademax = $grademaxoverrides[$itemid];
@@ -1236,6 +1246,19 @@ class grade_category extends grade_object {
                     }
                 }
 
+                // Now when we finally know the grademax we can adjust the automatic weights of extra credit items.
+                if (!$oldextracreditcalculation) {
+                    foreach ($grade_values as $itemid => $gradevalue) {
+                        if (!$items[$itemid]->weightoverride && isset($extracredititems[$itemid])) {
+                            $usergrademax = $items[$itemid]->grademax;
+                            if (isset($grademaxoverrides[$itemid])) {
+                                $usergrademax = $grademaxoverrides[$itemid];
+                            }
+                            $userweights[$itemid] = $grademax ? ($usergrademax / $grademax) : 0;
+                        }
+                    }
+                }
+
                 // We can use our freshly corrected weights below.
                 foreach ($grade_values as $itemid => $gradevalue) {
                     if (isset($extracredititems[$itemid])) {
@@ -1352,6 +1375,19 @@ class grade_category extends grade_object {
         $this->load_grade_item();
         $depends_on = $this->grade_item->depends_on();
 
+        // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
+        // wish to update the grades.
+        $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
+        // Only run if the gradebook isn't frozen.
+        if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
+            // Do nothing.
+        } else{
+            // Don't automatically update the max for calculated items.
+            if ($this->grade_item->is_calculated()) {
+                return;
+            }
+        }
+
         $items = false;
         if (!empty($depends_on)) {
             list($usql, $params) = $DB->get_in_or_equal($depends_on);
@@ -1517,6 +1553,11 @@ class grade_category extends grade_object {
 
         $totalnonoverriddengrademax = $totalgrademax - $totaloverriddengrademax;
 
+        // This setting indicates if we should use algorithm prior to MDL-49257 fix for calculating extra credit weights.
+        // Even though old algorith has bugs in it, we need to preserve existing grades.
+        $gradebookcalculationfreeze = (int)get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
+        $oldextracreditcalculation = $gradebookcalculationfreeze && ($gradebookcalculationfreeze <= 20150619);
+
         reset($children);
         foreach ($children as $sortorder => $child) {
             $gradeitem = null;
@@ -1539,6 +1580,16 @@ class grade_category extends grade_object {
                 continue;
             }
 
+            if (!$oldextracreditcalculation && $gradeitem->aggregationcoef > 0) {
+                // For an item with extra credit ignore other weigths and overrides.
+                // Do not change anything at all if it's weight was already overridden.
+                if (!$gradeitem->weightoverride) {
+                    $gradeitem->aggregationcoef2 = $totalgrademax ? ($gradeitem->grademax / $totalgrademax) : 0;
+                    $gradeitem->update();
+                }
+                continue;
+            }
+
             if (!$gradeitem->weightoverride) {
                 // Calculations with a grade maximum of zero will cause problems. Just set the weight to zero.
                 if ($totaloverriddenweight >= 1 || $totalnonoverriddengrademax == 0 || $gradeitem->grademax == 0) {
index 0ce19f9..5de6257 100644 (file)
@@ -350,11 +350,25 @@ class grade_grade extends grade_object {
         // When the following setting is turned on we use the grade_grade raw min and max values.
         $minmaxtouse = grade_get_setting($this->grade_item->courseid, 'minmaxtouse', $CFG->grade_minmaxtouse);
 
-        // Only aggregate items use separate min grades.
-        if ($minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE || $this->grade_item->is_aggregate_item()) {
-            return array($this->rawgrademin, $this->rawgrademax);
+        // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
+        // wish to update the grades.
+        $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->grade_item->courseid);
+        // Gradebook is frozen, run through old code.
+        if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
+            // Only aggregate items use separate min grades.
+            if ($minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE || $this->grade_item->is_aggregate_item()) {
+                return array($this->rawgrademin, $this->rawgrademax);
+            } else {
+                return array($this->grade_item->grademin, $this->grade_item->grademax);
+            }
         } else {
-            return array($this->grade_item->grademin, $this->grade_item->grademax);
+            // Only aggregate items use separate min grades, unless they are calculated grade items.
+            if (($this->grade_item->is_aggregate_item() && !$this->grade_item->is_calculated())
+                    || $minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE) {
+                return array($this->rawgrademin, $this->rawgrademax);
+            } else {
+                return array($this->grade_item->grademin, $this->grade_item->grademax);
+            }
         }
     }
 
index 4b7dc15..267e6ad 100644 (file)
@@ -1951,8 +1951,25 @@ class grade_item extends grade_object {
         // can not use own final grade during calculation
         unset($params['gi'.$this->id]);
 
+        // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
+        // wish to update the grades.
+        $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
+
+        $rawminandmaxchanged = false;
         // insert final grade - will be needed later anyway
         if ($oldgrade) {
+            // Only run through this code if the gradebook isn't frozen.
+            if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
+                // Do nothing.
+            } else {
+                // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
+                // grade_item grade maximum and minimum respectively.
+                if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) {
+                    $rawminandmaxchanged = true;
+                    $oldgrade->rawgrademax = $this->grademax;
+                    $oldgrade->rawgrademin = $this->grademin;
+                }
+            }
             $oldfinalgrade = $oldgrade->finalgrade;
             $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
             $grade->grade_item =& $this;
@@ -1960,6 +1977,16 @@ class grade_item extends grade_object {
         } else {
             $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
             $grade->grade_item =& $this;
+            $rawminandmaxchanged = false;
+            if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
+                // Do nothing.
+            } else {
+                // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
+                // grade_item grade maximum and minimum respectively.
+                $rawminandmaxchanged = true;
+                $grade->rawgrademax = $this->grademax;
+                $grade->rawgrademin = $this->grademin;
+            }
             $grade->insert('system');
             $oldfinalgrade = null;
         }
@@ -1986,17 +2013,30 @@ class grade_item extends grade_object {
                 // normalize
                 $grade->finalgrade = $this->bounded_grade($result);
             }
-
         }
 
-        // update in db if changed
-        if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
-            $grade->timemodified = time();
-            $success = $grade->update('compute');
+        // Only run through this code if the gradebook isn't frozen.
+        if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
+            // Update in db if changed.
+            if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
+                $grade->timemodified = time();
+                $success = $grade->update('compute');
 
-            // If successful trigger a user_graded event.
-            if ($success) {
-                \core\event\user_graded::create_from_grade($grade)->trigger();
+                // If successful trigger a user_graded event.
+                if ($success) {
+                    \core\event\user_graded::create_from_grade($grade)->trigger();
+                }
+            }
+        } else {
+            // Update in db if changed.
+            if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) {
+                $grade->timemodified = time();
+                $success = $grade->update('compute');
+
+                // If successful trigger a user_graded event.
+                if ($success) {
+                    \core\event\user_graded::create_from_grade($grade)->trigger();
+                }
             }
         }
 
index 683b7ee..c33fc8f 100644 (file)
@@ -1033,11 +1033,15 @@ function clean_param($param, $type) {
                 // Simulate the HTTPS version of the site.
                 $httpswwwroot = str_replace('http://', 'https://', $CFG->wwwroot);
 
-                if (preg_match(':^/:', $param)) {
+                if ($param === $CFG->wwwroot) {
+                    // Exact match;
+                } else if (!empty($CFG->loginhttps) && $param === $httpswwwroot) {
+                    // Exact match;
+                } else if (preg_match(':^/:', $param)) {
                     // Root-relative, ok!
-                } else if (preg_match('/^' . preg_quote($CFG->wwwroot, '/') . '/i', $param)) {
+                } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
                     // Absolute, and matches our wwwroot.
-                } else if (!empty($CFG->loginhttps) && preg_match('/^' . preg_quote($httpswwwroot, '/') . '/i', $param)) {
+                } else if (!empty($CFG->loginhttps) && preg_match('/^' . preg_quote($httpswwwroot . '/', '/') . '/i', $param)) {
                     // Absolute, and matches our httpswwwroot.
                 } else {
                     // Relative - let's make sure there are no tricks.
index 8f3179e..a00b5ec 100644 (file)
@@ -316,8 +316,6 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
                 $tree->add_node($node);
             }
         }
-
-        echo html_writer::end_tag('dl');
     }
 
     if ($user->icq && !isset($hiddenfields['icqnumber'])) {
index 0ecca8a..549eb6d 100644 (file)
@@ -2604,7 +2604,7 @@ class global_navigation extends navigation_node {
         $coursenode->add('frontpageloaded', null, self::TYPE_CUSTOM, null, 'frontpageloaded')->display = false;
 
         //Participants
-        if (has_capability('moodle/course:viewparticipants',  context_system::instance())) {
+        if (has_capability('moodle/site:viewparticipants',  context_system::instance())) {
             $coursenode->add(get_string('participants'), new moodle_url('/user/index.php?id='.$course->id), self::TYPE_CUSTOM, get_string('participants'), 'participants');
         }
 
index 912557c..2a2d106 100644 (file)
@@ -220,6 +220,52 @@ function match_grade_options($gradeoptionsfull, $grade, $matchgrades = 'error')
     }
 }
 
+/**
+ * Category is about to be deleted,
+ * 1/ All questions are deleted for this question category.
+ * 2/ Any questions that can't be deleted are moved to a new category
+ * NOTE: this function is called from lib/db/upgrade.php
+ *
+ * @param object|coursecat $category course category object
+ */
+function question_category_delete_safe($category) {
+    global $DB;
+    $criteria = array('category' => $category->id);
+    $context = context::instance_by_id($category->contextid, IGNORE_MISSING);
+    $rescue = null; // See the code around the call to question_save_from_deletion.
+
+    // Deal with any questions in the category.
+    if ($questions = $DB->get_records('question', $criteria, '', 'id,qtype')) {
+
+        // Try to delete each question.
+        foreach ($questions as $question) {
+            question_delete_question($question->id);
+        }
+
+        // Check to see if there were any questions that were kept because
+        // they are still in use somehow, even though quizzes in courses
+        // in this category will already have been deleted. This could
+        // happen, for example, if questions are added to a course,
+        // and then that course is moved to another category (MDL-14802).
+        $questionids = $DB->get_records_menu('question', $criteria, '', 'id, 1');
+        if (!empty($questionids)) {
+            $parentcontextid = SYSCONTEXTID;
+            $name = get_string('unknown', 'question');
+            if ($context !== false) {
+                $name = $context->get_context_name();
+                $parentcontext = $context->get_parent_context();
+                if ($parentcontext) {
+                    $parentcontextid = $parentcontext->id;
+                }
+            }
+            question_save_from_deletion(array_keys($questionids), $parentcontextid, $name, $rescue);
+        }
+    }
+
+    // Now delete the category.
+    $DB->delete_records('question_categories', array('id' => $category->id));
+}
+
 /**
  * Tests whether any question in a category is used by any part of Moodle.
  *
@@ -306,54 +352,52 @@ function question_delete_question($questionid) {
 }
 
 /**
- * All question categories and their questions are deleted for this course.
+ * All question categories and their questions are deleted for this context id.
  *
- * @param stdClass $course an object representing the activity
- * @param boolean $feedback to specify if the process must output a summary of its work
- * @return boolean
+ * @param object $contextid The contextid to delete question categories from
+ * @return array Feedback from deletes (if any)
  */
-function question_delete_course($course, $feedback=true) {
-    global $DB, $OUTPUT;
+function question_delete_context($contextid) {
+    global $DB;
 
     //To store feedback to be showed at the end of the process
     $feedbackdata   = array();
 
     //Cache some strings
     $strcatdeleted = get_string('unusedcategorydeleted', 'question');
-    $coursecontext = context_course::instance($course->id);
-    $categoriescourse = $DB->get_records('question_categories',
-            array('contextid' => $coursecontext->id), 'parent', 'id, parent, name, contextid');
-
-    if ($categoriescourse) {
-
+    $fields = 'id, parent, name, contextid';
+    if ($categories = $DB->get_records('question_categories', array('contextid' => $contextid), 'parent', $fields)) {
         //Sort categories following their tree (parent-child) relationships
         //this will make the feedback more readable
-        $categoriescourse = sort_categories_by_tree($categoriescourse);
-
-        foreach ($categoriescourse as $category) {
+        $categories = sort_categories_by_tree($categories);
 
-            //Delete it completely (questions and category itself)
-            //deleting questions
-            if ($questions = $DB->get_records('question',
-                    array('category' => $category->id), '', 'id,qtype')) {
-                foreach ($questions as $question) {
-                    question_delete_question($question->id);
-                }
-                $DB->delete_records("question", array("category" => $category->id));
-            }
-            //delete the category
-            $DB->delete_records('question_categories', array('id' => $category->id));
+        foreach ($categories as $category) {
+            question_category_delete_safe($category);
 
             //Fill feedback
             $feedbackdata[] = array($category->name, $strcatdeleted);
         }
-        //Inform about changes performed if feedback is enabled
-        if ($feedback) {
-            $table = new html_table();
-            $table->head = array(get_string('category', 'question'), get_string('action'));
-            $table->data = $feedbackdata;
-            echo html_writer::table($table);
-        }
+    }
+    return $feedbackdata;
+}
+
+/**
+ * All question categories and their questions are deleted for this course.
+ *
+ * @param stdClass $course an object representing the activity
+ * @param boolean $feedback to specify if the process must output a summary of its work
+ * @return boolean
+ */
+function question_delete_course($course, $feedback=true) {
+    $coursecontext = context_course::instance($course->id);
+    $feedbackdata = question_delete_context($coursecontext->id, $feedback);
+
+    // Inform about changes performed if feedback is enabled.
+    if ($feedback && $feedbackdata) {
+        $table = new html_table();
+        $table->head = array(get_string('category', 'question'), get_string('action'));
+        $table->data = $feedbackdata;
+        echo html_writer::table($table);
     }
     return true;
 }
@@ -374,58 +418,10 @@ function question_delete_course_category($category, $newcategory, $feedback=true
 
     $context = context_coursecat::instance($category->id);
     if (empty($newcategory)) {
-        $feedbackdata   = array(); // To store feedback to be showed at the end of the process
-        $rescueqcategory = null; // See the code around the call to question_save_from_deletion.
-        $strcatdeleted = get_string('unusedcategorydeleted', 'question');
-
-        // Loop over question categories.
-        if ($categories = $DB->get_records('question_categories',
-                array('contextid'=>$context->id), 'parent', 'id, parent, name')) {
-            foreach ($categories as $category) {
-
-                // Deal with any questions in the category.
-                if ($questions = $DB->get_records('question',
-                        array('category' => $category->id), '', 'id,qtype')) {
-
-                    // Try to delete each question.
-                    foreach ($questions as $question) {
-                        question_delete_question($question->id);
-                    }
-
-                    // Check to see if there were any questions that were kept because
-                    // they are still in use somehow, even though quizzes in courses
-                    // in this category will already have been deleted. This could
-                    // happen, for example, if questions are added to a course,
-                    // and then that course is moved to another category (MDL-14802).
-                    $questionids = $DB->get_records_menu('question',
-                            array('category'=>$category->id), '', 'id, 1');
-                    if (!empty($questionids)) {
-                        $parentcontextid = false;
-                        $parentcontext = $context->get_parent_context();
-                        if ($parentcontext) {
-                            $parentcontextid = $parentcontext->id;
-                        }
-                        if (!$rescueqcategory = question_save_from_deletion(
-                                array_keys($questionids), $parentcontextid,
-                                $context->get_context_name(), $rescueqcategory)) {
-                            return false;
-                        }
-                        $feedbackdata[] = array($category->name,
-                            get_string('questionsmovedto', 'question', $rescueqcategory->name));
-                    }
-                }
-
-                // Now delete the category.
-                if (!$DB->delete_records('question_categories', array('id'=>$category->id))) {
-                    return false;
-                }
-                $feedbackdata[] = array($category->name, $strcatdeleted);
-
-            } // End loop over categories.
-        }
+        $feedbackdata = question_delete_context($context->id, $feedback);
 
         // Output feedback if requested.
-        if ($feedback and $feedbackdata) {
+        if ($feedback && $feedbackdata) {
             $table = new html_table();
             $table->head = array(get_string('questioncategory', 'question'), get_string('action'));
             $table->data = $feedbackdata;
@@ -460,7 +456,7 @@ function question_delete_course_category($category, $newcategory, $feedback=true
  * Enter description here...
  *
  * @param array $questionids of question ids
- * @param object $newcontext the context to create the saved category in.
+ * @param object $newcontextid the context to create the saved category in.
  * @param string $oldplace a textual description of the think being deleted,
  *      e.g. from get_context_name
  * @param object $newcategory
@@ -497,44 +493,16 @@ function question_save_from_deletion($questionids, $newcontextid, $oldplace,
  * @return boolean
  */
 function question_delete_activity($cm, $feedback=true) {
-    global $DB, $OUTPUT;
-
-    //To store feedback to be showed at the end of the process
-    $feedbackdata   = array();
+    global $DB;
 
-    //Cache some strings
-    $strcatdeleted = get_string('unusedcategorydeleted', 'question');
     $modcontext = context_module::instance($cm->id);
-    if ($categoriesmods = $DB->get_records('question_categories',
-            array('contextid' => $modcontext->id), 'parent', 'id, parent, name, contextid')) {
-        //Sort categories following their tree (parent-child) relationships
-        //this will make the feedback more readable
-        $categoriesmods = sort_categories_by_tree($categoriesmods);
-
-        foreach ($categoriesmods as $category) {
-
-            //Delete it completely (questions and category itself)
-            //deleting questions
-            if ($questions = $DB->get_records('question',
-                    array('category' => $category->id), '', 'id,qtype')) {
-                foreach ($questions as $question) {
-                    question_delete_question($question->id);
-                }
-                $DB->delete_records("question", array("category"=>$category->id));
-            }
-            //delete the category
-            $DB->delete_records('question_categories', array('id'=>$category->id));
-
-            //Fill feedback
-            $feedbackdata[] = array($category->name, $strcatdeleted);
-        }
-        //Inform about changes performed if feedback is enabled
-        if ($feedback) {
-            $table = new html_table();
-            $table->head = array(get_string('category', 'question'), get_string('action'));
-            $table->data = $feedbackdata;
-            echo html_writer::table($table);
-        }
+    $feedbackdata = question_delete_context($modcontext->id, $feedback);
+    // Inform about changes performed if feedback is enabled.
+    if ($feedback && $feedbackdata) {
+        $table = new html_table();
+        $table->head = array(get_string('category', 'question'), get_string('action'));
+        $table->data = $feedbackdata;
+        echo html_writer::table($table);
     }
     return true;
 }
@@ -1343,7 +1311,7 @@ function question_has_capability_on($question, $cap, $cachecat = -1) {
     if (!isset($categories[$question->category])) {
         if (!$categories[$question->category] = $DB->get_record('question_categories',
                 array('id'=>$question->category))) {
-            print_error('invalidcategory', 'quiz');
+            print_error('invalidcategory', 'question');
         }
     }
     $category = $categories[$question->category];
index de9db16..7bfd3c0 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+/**
+ * Composer error exit status.
+ *
+ * @var int
+ */
+define('TESTING_EXITCODE_COMPOSER', 255);
+
 /**
  * Returns relative path against current working directory,
  * to be used for shell execution hints.
@@ -170,22 +177,54 @@ function testing_error($errorcode, $text = '') {
  * @return void exit() if something goes wrong
  */
 function testing_update_composer_dependencies() {
-
     // To restore the value after finishing.
     $cwd = getcwd();
 
-    // Dirroot.
-    chdir(__DIR__ . '/../..');
-
-    // Download composer.phar if we can.
-    if (!file_exists(__DIR__ . '/../../composer.phar')) {
-        passthru("curl http://getcomposer.org/installer | php", $code);
-        if ($code != 0) {
-            exit($code);
+    // Set some paths.
+    $dirroot = dirname(dirname(__DIR__));
+    $composerpath = $dirroot . DIRECTORY_SEPARATOR . 'composer.phar';
+    $composerurl = 'https://getcomposer.org/composer.phar';
+
+    // Switch to Moodle's dirroot for easier path handling.
+    chdir($dirroot);
+
+    // Download or update composer.phar. Unfortunately we can't use the curl
+    // class in filelib.php as we're running within one of the test platforms.
+    if (!file_exists($composerpath)) {
+        $file = @fopen($composerpath, 'w');
+        if ($file === false) {
+            $errordetails = error_get_last();
+            $error = sprintf("Unable to create composer.phar\nPHP error: %s",
+                             $errordetails['message']);
+            testing_error(TESTING_EXITCODE_COMPOSER, $error);
+        }
+        $curl = curl_init();
+
+        curl_setopt($curl, CURLOPT_URL,  $composerurl);
+        curl_setopt($curl, CURLOPT_FILE, $file);
+        $result = curl_exec($curl);
+
+        $curlerrno = curl_errno($curl);
+        $curlerror = curl_error($curl);
+        $curlinfo = curl_getinfo($curl);
+
+        curl_close($curl);
+        fclose($file);
+
+        if (!$result) {
+            $error = sprintf("Unable to download composer.phar\ncURL error (%d): %s",
+                             $curlerrno, $curlerror);
+            testing_error(TESTING_EXITCODE_COMPOSER, $error);
+        } else if ($curlinfo['http_code'] === 404) {
+            if (file_exists($composerpath)) {
+                // Deleting the resource as it would contain HTML.
+                unlink($composerpath);
+            }
+            $error = sprintf("Unable to download composer.phar\n" .
+                                "404 http status code fetching $composerurl");
+            testing_error(TESTING_EXITCODE_COMPOSER, $error);
         }
     } else {
-
-        // If it is already there update the installer.
         passthru("php composer.phar self-update", $code);
         if ($code != 0) {
             exit($code);
@@ -198,5 +237,6 @@ function testing_update_composer_dependencies() {
         exit($code);
     }
 
+    // Return to our original location.
     chdir($cwd);
 }
index 22628a4..932dec3 100644 (file)
@@ -394,7 +394,17 @@ class behat_forms extends behat_base {
         );
 
         if (!$this->running_javascript()) {
-            $actions[] = new Given('I press "' . get_string('go') . '"');
+            // Press button in the specified select container.
+            $containerxpath = "//div[" .
+                "(contains(concat(' ', normalize-space(@class), ' '), ' singleselect ') " .
+                    "or contains(concat(' ', normalize-space(@class), ' '), ' urlselect ')".
+                ") and (
+                .//label[contains(normalize-space(string(.)), '" . $singleselect . "')] " .
+                    "or .//select[(./@name='" . $singleselect . "' or ./@id='". $singleselect . "')]" .
+                ")]";
+
+            $actions[] = new Given('I click on "' . get_string('go') . '" "button" in the "' . $containerxpath .
+                '" "xpath_element"');
         }
 
         return $actions;
index cab417d..b32c328 100644 (file)
@@ -878,4 +878,120 @@ EOF;
         $this->assertEquals('image', $mimeinfo['png']['string']);
         $this->assertEquals(true, $mimeinfo['txt']['defaulticon']);
     }
+
+    /**
+     * Test curl agent settings.
+     */
+    public function test_curl_useragent() {
+        $curl = new testable_curl();
+        $options = $curl->get_options();
+        $this->assertNotEmpty($options);
+
+        $curl->call_apply_opt($options);
+        $this->assertTrue(in_array('User-Agent: MoodleBot/1.0', $curl->header));
+        $this->assertFalse(in_array('User-Agent: Test/1.0', $curl->header));
+
+        $options['CURLOPT_USERAGENT'] = 'Test/1.0';
+        $curl->call_apply_opt($options);
+        $this->assertTrue(in_array('User-Agent: Test/1.0', $curl->header));
+        $this->assertFalse(in_array('User-Agent: MoodleBot/1.0', $curl->header));
+
+        $curl->set_option('CURLOPT_USERAGENT', 'AnotherUserAgent/1.0');
+        $curl->call_apply_opt();
+        $this->assertTrue(in_array('User-Agent: AnotherUserAgent/1.0', $curl->header));
+        $this->assertFalse(in_array('User-Agent: Test/1.0', $curl->header));
+
+        $curl->set_option('CURLOPT_USERAGENT', 'AnotherUserAgent/1.1');
+        $options = $curl->get_options();
+        $curl->call_apply_opt($options);
+        $this->assertTrue(in_array('User-Agent: AnotherUserAgent/1.1', $curl->header));
+        $this->assertFalse(in_array('User-Agent: AnotherUserAgent/1.0', $curl->header));
+
+        $curl->unset_option('CURLOPT_USERAGENT');
+        $curl->call_apply_opt();
+        $this->assertTrue(in_array('User-Agent: MoodleBot/1.0', $curl->header));
+
+        // Finally, test it via exttests, to ensure the agent is sent properly.
+        // Matching.
+        $testurl = $this->getExternalTestFileUrl('/test_agent.php');
+        $extcurl = new curl();
+        $contents = $extcurl->get($testurl, array(), array('CURLOPT_USERAGENT' => 'AnotherUserAgent/1.2'));
+        $response = $extcurl->getResponse();
+        $this->assertSame('200 OK', reset($response));
+        $this->assertSame(0, $extcurl->get_errno());
+        $this->assertSame('OK', $contents);
+        // Not matching.
+        $contents = $extcurl->get($testurl, array(), array('CURLOPT_USERAGENT' => 'NonMatchingUserAgent/1.2'));
+        $response = $extcurl->getResponse();
+        $this->assertSame('200 OK', reset($response));
+        $this->assertSame(0, $extcurl->get_errno());
+        $this->assertSame('', $contents);
+    }
+}
+
+/**
+ * Test-specific class to allow easier testing of curl functions.
+ *
+ * @copyright 2015 Dave Cooper
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_curl extends curl {
+    /**
+     * Accessor for private options array using reflection.
+     *
+     * @return array
+     */
+    public function get_options() {
+        // Access to private property.
+        $rp = new ReflectionProperty('curl', 'options');
+        $rp->setAccessible(true);
+        return $rp->getValue($this);
+    }
+
+    /**
+     * Setter for private options array using reflection.
+     *
+     * @param array $options
+     */
+    public function set_options($options) {
+        // Access to private property.
+        $rp = new ReflectionProperty('curl', 'options');
+        $rp->setAccessible(true);
+        $rp->setValue($this, $options);
+    }
+
+    /**
+     * Setter for individual option.
+     * @param string $option
+     * @param string $value
+     */
+    public function set_option($option, $value) {
+        $options = $this->get_options();
+        $options[$option] = $value;
+        $this->set_options($options);
+    }
+
+    /**
+     * Unsets an option on the curl object
+     * @param string $option
+     */
+    public function unset_option($option) {
+        $options = $this->get_options();
+        unset($options[$option]);
+        $this->set_options($options);
+    }
+
+    /**
+     * Wrapper to access the private curl::apply_opt() method using reflection.
+     *
+     * @param array $options
+     * @return resource The curl handle
+     */
+    public function call_apply_opt($options = null) {
+        // Access to private method.
+        $rm = new ReflectionMethod('curl', 'apply_opt');
+        $rm->setAccessible(true);
+        $ch = curl_init();
+        return $rm->invoke($this, $ch, $options);
+    }
 }
diff --git a/lib/tests/filter_manager_test.php b/lib/tests/filter_manager_test.php
new file mode 100644 (file)
index 0000000..85b7632
--- /dev/null
@@ -0,0 +1,82 @@
+<?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/>.
+
+/**
+ * Unit tests for the {@link filter_manager} class.
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2015 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Unit tests for the {@link filter_manager} class.
+ *
+ * @copyright 2015 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+class core_filter_manager_testcase extends advanced_testcase {
+
+    /**
+     * Helper method to apply filters to some text and return the result.
+     * @param string $text the text to filter.
+     * @param array $skipfilters any filters not to apply, even if they are configured.
+     * @return string the filtered text.
+     */
+    protected function filter_text($text, $skipfilters) {
+        global $PAGE;
+        $filtermanager = filter_manager::instance();
+        $filtermanager->setup_page_for_filters($PAGE, $PAGE->context);
+        $filteroptions = array(
+                'originalformat' => FORMAT_HTML,
+                'noclean' => false,
+        );
+        return $filtermanager->filter_text($text, $PAGE->context, $filteroptions, $skipfilters);
+    }
+
+    public function test_filter_normal() {
+        $this->resetAfterTest();
+        filter_set_global_state('emoticon', TEXTFILTER_ON);
+        $this->assertRegExp('~^<p><img class="emoticon" alt="smile" ([^>]+)></p>$~',
+                $this->filter_text('<p>:-)</p>', array()));
+    }
+
+    public function test_one_filter_disabled() {
+        $this->resetAfterTest();
+        filter_set_global_state('emoticon', TEXTFILTER_ON);
+        $this->assertEquals('<p>:-)</p>',
+                $this->filter_text('<p>:-)</p>', array('emoticon')));
+    }
+
+    public function test_disabling_other_filter_does_not_break_it() {
+        $this->resetAfterTest();
+        filter_set_global_state('emoticon', TEXTFILTER_ON);
+        $this->assertRegExp('~^<p><img class="emoticon" alt="smile" ([^>]+)></p>$~',
+                $this->filter_text('<p>:-)</p>', array('urltolink')));
+    }
+
+    public function test_one_filter_of_two_disabled() {
+        $this->resetAfterTest();
+        filter_set_global_state('emoticon', TEXTFILTER_ON);
+        filter_set_global_state('urltolink', TEXTFILTER_ON);
+        $this->assertRegExp('~^<p><img class="emoticon" alt="smile" ([^>]+)> http://google.com/</p>$~',
+                $this->filter_text('<p>:-) http://google.com/</p>', array('glossary', 'urltolink')));
+    }
+}
index 7c82265..3d33588 100644 (file)
@@ -583,6 +583,9 @@ class core_moodlelib_testcase extends advanced_testcase {
 
     public function test_clean_param_localurl() {
         global $CFG;
+
+        $this->resetAfterTest();
+
         // External, invalid.
         $this->assertSame('', clean_param('funny:thing', PARAM_LOCALURL));
         $this->assertSame('', clean_param('http://google.com/', PARAM_LOCALURL));
@@ -600,7 +603,6 @@ class core_moodlelib_testcase extends advanced_testcase {
 
         // Local absolute HTTPS.
         $httpsroot = str_replace('http:', 'https:', $CFG->wwwroot);
-        $initialloginhttps = $CFG->loginhttps;
         $CFG->loginhttps = false;
         $this->assertSame('', clean_param($httpsroot, PARAM_LOCALURL));
         $this->assertSame('', clean_param($httpsroot . '/with/something?else=true', PARAM_LOCALURL));
@@ -608,7 +610,13 @@ class core_moodlelib_testcase extends advanced_testcase {
         $this->assertSame($httpsroot, clean_param($httpsroot, PARAM_LOCALURL));
         $this->assertSame($httpsroot . '/with/something?else=true',
             clean_param($httpsroot . '/with/something?else=true', PARAM_LOCALURL));
-        $CFG->loginhttps = $initialloginhttps;
+
+        // Test open redirects are not possible.
+        $CFG->loginhttps = false;
+        $CFG->wwwroot = 'http://www.example.com';
+        $this->assertSame('', clean_param('http://www.example.com.evil.net/hack.php', PARAM_LOCALURL));
+        $CFG->loginhttps = true;
+        $this->assertSame('', clean_param('https://www.example.com.evil.net/hack.php', PARAM_LOCALURL));
     }
 
     public function test_clean_param_file() {
index 94bb71e..6455616 100644 (file)
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
 global $CFG;
 
 require_once($CFG->libdir . '/questionlib.php');
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
 require_once($CFG->dirroot . '/tag/lib.php');
 
 // Get the necessary files to perform backup and restore.
@@ -58,6 +59,61 @@ class core_questionlib_testcase extends advanced_testcase {
         gc_collect_cycles();
     }
 
+    /**
+     * Return true and false to test functions with feedback on and off.
+     *
+     * @return array Test data
+     */
+    public function provider_feedback() {
+        return array(
+            'Feedback test' => array(true),
+            'No feedback test' => array(false)
+        );
+    }
+
+    /**
+     * Setup a course, a quiz, a question category and a question for testing.
+     *
+     * @param string $type The type of question category to create.
+     * @return array The created data objects
+     */
+    public function setup_quiz_and_questions($type = 'module') {
+        // Create course category.
+        $category = $this->getDataGenerator()->create_category();
+
+        // Create course.
+        $course = $this->getDataGenerator()->create_course(array('numsections' => 5));
+
+        $options = array(
+            'course' => $course->id,
+            'duedate' => time(),
+        );
+
+        // Generate an assignment with due date (will generate a course event).
+        $quiz = $this->getDataGenerator()->create_module('quiz', $options);
+
+        $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
+
+        if ('course' == $type) {
+            $context = context_course::instance($course->id);
+        } else if ('category' == $type) {
+            $context = context_coursecat::instance($category->id);
+        } else {
+            $context = context_module::instance($quiz->cmid);
+        }
+
+        $qcat = $qgen->create_question_category(array('contextid' => $context->id));
+
+        $questions = array(
+                $qgen->create_question('shortanswer', null, array('category' => $qcat->id)),
+                $qgen->create_question('shortanswer', null, array('category' => $qcat->id)),
+        );
+
+        quiz_add_quiz_question($questions[0]->id, $quiz);
+
+        return array($category, $course, $quiz, $qcat, $questions);
+    }
+
     public function test_question_reorder_qtypes() {
         $this->assertEquals(
             array(0 => 't2', 1 => 't1', 2 => 't3'),
@@ -205,4 +261,140 @@ class core_questionlib_testcase extends advanced_testcase {
         // Check that there are two questions in the restored to course's context.
         $this->assertEquals(2, $DB->count_records('question', array('category' => $restoredcategory->id)));
     }
+
+    /**
+     * This function tests the question_category_delete_safe function.
+     */
+    public function test_question_category_delete_safe() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
+
+        question_category_delete_safe($qcat);
+
+        // Verify category deleted.
+        $criteria = array('id' => $qcat->id);
+        $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
+
+        // Verify questions deleted or moved.
+        $criteria = array('category' => $qcat->id);
+        $this->assertEquals(0, $DB->count_records('question', $criteria));
+
+        // Verify question not deleted.
+        $criteria = array('id' => $questions[0]->id);
+        $this->assertEquals(1, $DB->count_records('question', $criteria));
+    }
+
+    /**
+     * This function tests the question_delete_activity function.
+     *
+     * @param bool $feedback Whether to return feedback
+     * @dataProvider provider_feedback
+     */
+    public function test_question_delete_activity($feedback) {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
+
+        $cm = get_coursemodule_from_instance('quiz', $quiz->id);
+        // Test that the feedback works.
+        if ($feedback) {
+            $this->expectOutputRegex('|'.get_string('unusedcategorydeleted', 'question').'|');
+        }
+        question_delete_activity($cm, $feedback);
+
+        // Verify category deleted.
+        $criteria = array('id' => $qcat->id);
+        $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
+
+        // Verify questions deleted or moved.
+        $criteria = array('category' => $qcat->id);
+        $this->assertEquals(0, $DB->count_records('question', $criteria));
+    }
+
+    /**
+     * This function tests the question_delete_context function.
+     */
+    public function test_question_delete_context() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
+
+        // Get the module context id.
+        $result = question_delete_context($qcat->contextid);
+
+        // Verify category deleted.
+        $criteria = array('id' => $qcat->id);
+        $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
+
+        // Verify questions deleted or moved.
+        $criteria = array('category' => $qcat->id);
+        $this->assertEquals(0, $DB->count_records('question', $criteria));
+
+        // Test that the feedback works.
+        $expected[] = array($qcat->name, get_string('unusedcategorydeleted', 'question'));
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * This function tests the question_delete_course function.
+     *
+     * @param bool $feedback Whether to return feedback
+     * @dataProvider provider_feedback
+     */
+    public function test_question_delete_course($feedback) {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
+
+        // Test that the feedback works.
+        if ($feedback) {
+            $this->expectOutputRegex('|'.get_string('unusedcategorydeleted', 'question').'|');
+        }
+        question_delete_course($course, $feedback);
+
+        // Verify category deleted.
+        $criteria = array('id' => $qcat->id);
+        $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
+
+        // Verify questions deleted or moved.
+        $criteria = array('category' => $qcat->id);
+        $this->assertEquals(0, $DB->count_records('question', $criteria));
+    }
+
+    /**
+     * This function tests the question_delete_course_category function.
+     *
+     * @param bool $feedback Whether to return feedback
+     * @dataProvider provider_feedback
+     */
+    public function test_question_delete_course_category($feedback) {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
+
+        // Test that the feedback works.
+        if ($feedback) {
+            $this->expectOutputRegex('|'.get_string('unusedcategorydeleted', 'question').'|');
+        }
+        question_delete_course_category($category, 0, $feedback);
+
+        // Verify category deleted.
+        $criteria = array('id' => $qcat->id);
+        $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
+
+        // Verify questions deleted or moved.
+        $criteria = array('category' => $qcat->id);
+        $this->assertEquals(0, $DB->count_records('question', $criteria));
+    }
 }
index debdffd..fc06095 100644 (file)
@@ -482,4 +482,263 @@ class core_upgradelib_testcase extends advanced_testcase {
         // Restore value.
         $CFG->grade_minmaxtouse = $initialminmax;
     }
+
+    public function test_upgrade_extra_credit_weightoverride() {
+        global $DB, $CFG;
+
+        $this->resetAfterTest(true);
+
+        $c = array();
+        $a = array();
+        $gi = array();
+        for ($i=0; $i<5; $i++)&nb