Merge branch 'MDL-38106-master' of git://github.com/danpoltawski/moodle
authorMarina Glancy <marina@moodle.com>
Tue, 7 Oct 2014 02:25:29 +0000 (10:25 +0800)
committerMarina Glancy <marina@moodle.com>
Tue, 7 Oct 2014 02:25:29 +0000 (10:25 +0800)
207 files changed:
admin/environment.xml
admin/roles/classes/potential_assignees_below_course.php
admin/settings/grades.php
admin/tool/langimport/classes/controller.php [new file with mode: 0644]
admin/tool/langimport/classes/event/langpack_imported.php [new file with mode: 0644]
admin/tool/langimport/classes/event/langpack_removed.php [new file with mode: 0644]
admin/tool/langimport/classes/event/langpack_updated.php [new file with mode: 0644]
admin/tool/langimport/classes/task/update_langpacks_task.php [new file with mode: 0644]
admin/tool/langimport/db/tasks.php [new file with mode: 0644]
admin/tool/langimport/index.php
admin/tool/langimport/lang/en/tool_langimport.php
admin/tool/langimport/tests/behat/behat_tool_langimport.php [new file with mode: 0644]
admin/tool/langimport/tests/behat/manage_langpacks.feature [new file with mode: 0644]
admin/tool/langimport/tests/events_test.php [new file with mode: 0644]
admin/tool/langimport/version.php
backup/moodle2/backup_stepslib.php
course/classes/management/helper.php
course/classes/management_renderer.php
course/management.php
course/renderer.php
course/reset_form.php
course/tests/behat/course_search.feature [new file with mode: 0644]
grade/edit/scale/edit.php
grade/edit/settings/index.php
grade/edit/tree/action.php
grade/edit/tree/calculation.php
grade/edit/tree/category.php
grade/edit/tree/category_form.php
grade/edit/tree/functions.js
grade/edit/tree/grade_form.php
grade/edit/tree/index.php
grade/edit/tree/item.php
grade/edit/tree/item_form.php
grade/edit/tree/lib.php
grade/edit/tree/outcomeitem.php
grade/lib.php
grade/report/grader/index.php
grade/report/grader/lib.php
grade/report/lib.php
grade/report/overview/lib.php
grade/report/user/lib.php
grade/report/user/settings.php
grade/report/user/styles.css
grade/tests/behat/behat_grade.php
grade/tests/behat/grade_aggregation.feature
grade/tests/behat/grade_calculated_weights.feature [new file with mode: 0644]
grade/tests/behat/grade_mingrade.feature [new file with mode: 0644]
grade/tests/behat/grade_natural_normalisation.feature [new file with mode: 0644]
grade/tests/behat/grade_scales.feature [new file with mode: 0644]
grade/tests/behat/grade_view.feature
grade/tests/edittreelib_test.php
grade/tests/report_graderlib_test.php
grade/tests/reportlib_test.php
lang/en/deprecated.txt
lang/en/grades.php
lib/classes/grades_external.php
lib/classes/task/scheduled_task.php
lib/db/install.xml [changed mode: 0644->0755]
lib/db/services.php
lib/db/upgrade.php
lib/grade/constants.php
lib/grade/grade_category.php
lib/grade/grade_grade.php
lib/grade/grade_item.php
lib/gradelib.php
lib/phpunit/classes/util.php
lib/testing/classes/util.php
lib/tests/behat/behat_general.php
lib/tests/scheduled_task_test.php
lib/upgrade.txt
message/externallib.php
message/lib.php
message/tests/externallib_test.php
mod/forum/classes/existing_subscriber_selector.php
mod/forum/classes/potential_subscriber_selector.php
mod/forum/classes/subscriptions.php
mod/forum/discuss.php
mod/forum/externallib.php
mod/forum/lib.php
mod/forum/tests/behat/forum_subscriptions_availability.feature [new file with mode: 0644]
mod/quiz/addrandom.php
mod/quiz/addrandomform.php
mod/quiz/attemptlib.php
mod/quiz/classes/admin_review_setting.php
mod/quiz/classes/output/edit_renderer.php [new file with mode: 0644]
mod/quiz/classes/repaginate.php [new file with mode: 0644]
mod/quiz/classes/structure.php [new file with mode: 0644]
mod/quiz/edit.js [deleted file]
mod/quiz/edit.php
mod/quiz/edit_rest.php [new file with mode: 0644]
mod/quiz/editlib.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/mod_form.php
mod/quiz/questionbank.ajax.php [new file with mode: 0644]
mod/quiz/renderer.php
mod/quiz/repaginate.php [new file with mode: 0644]
mod/quiz/styles.css
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/tests/behat/editing_add.feature [new file with mode: 0644]
mod/quiz/tests/behat/editing_click_move_icon.feature [new file with mode: 0644]
mod/quiz/tests/behat/editing_repaginate.feature [new file with mode: 0644]
mod/quiz/tests/behat/editing_set_marks.feature [new file with mode: 0644]
mod/quiz/tests/editlib_test.php [deleted file]
mod/quiz/tests/lib_test.php
mod/quiz/tests/locallib_test.php
mod/quiz/tests/repaginate_test.php [new file with mode: 0644]
mod/quiz/tests/structure_test.php [new file with mode: 0644]
mod/quiz/upgrade.txt
mod/quiz/version.php
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-modform/moodle-mod_quiz-modform-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-modform/moodle-mod_quiz-modform-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-modform/moodle-mod_quiz-modform.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-questionchooser/moodle-mod_quiz-questionchooser-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-questionchooser/moodle-mod_quiz-questionchooser-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-questionchooser/moodle-mod_quiz-questionchooser.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-quizquestionbank/moodle-mod_quiz-quizquestionbank-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-quizquestionbank/moodle-mod_quiz-quizquestionbank-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-quizquestionbank/moodle-mod_quiz-quizquestionbank.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-randomquestion/moodle-mod_quiz-randomquestion-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-randomquestion/moodle-mod_quiz-randomquestion-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-randomquestion/moodle-mod_quiz-randomquestion.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-repaginate/moodle-mod_quiz-repaginate-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-repaginate/moodle-mod_quiz-repaginate-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-repaginate/moodle-mod_quiz-repaginate.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-base/moodle-mod_quiz-util-base-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-base/moodle-mod_quiz-util-base-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-base/moodle-mod_quiz-util-base.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js [new file with mode: 0644]
mod/quiz/yui/src/dragdrop/build.json [new file with mode: 0644]
mod/quiz/yui/src/dragdrop/js/dragdrop.js [new file with mode: 0644]
mod/quiz/yui/src/dragdrop/js/resource.js [new file with mode: 0644]
mod/quiz/yui/src/dragdrop/js/section.js [new file with mode: 0644]
mod/quiz/yui/src/dragdrop/meta/dragdrop.json [new file with mode: 0644]
mod/quiz/yui/src/modform/build.json [new file with mode: 0644]
mod/quiz/yui/src/modform/js/modform.js [new file with mode: 0644]
mod/quiz/yui/src/modform/meta/modform.json [new file with mode: 0644]
mod/quiz/yui/src/questionchooser/build.json [new file with mode: 0644]
mod/quiz/yui/src/questionchooser/js/questionchooser.js [new file with mode: 0644]
mod/quiz/yui/src/questionchooser/meta/questionchooser.json [new file with mode: 0644]
mod/quiz/yui/src/quizbase/build.json [new file with mode: 0644]
mod/quiz/yui/src/quizbase/js/quizbase.js [new file with mode: 0644]
mod/quiz/yui/src/quizbase/meta/quizbase.json [new file with mode: 0644]
mod/quiz/yui/src/quizquestionbank/build.json [new file with mode: 0644]
mod/quiz/yui/src/quizquestionbank/js/quizquestionbank.js [new file with mode: 0644]
mod/quiz/yui/src/quizquestionbank/meta/quizquestionbank.json [new file with mode: 0644]
mod/quiz/yui/src/randomquestion/build.json [new file with mode: 0644]
mod/quiz/yui/src/randomquestion/js/randomquestion.js [new file with mode: 0644]
mod/quiz/yui/src/randomquestion/meta/randomquestion.json [new file with mode: 0644]
mod/quiz/yui/src/repaginate/build.json [new file with mode: 0644]
mod/quiz/yui/src/repaginate/js/repaginate.js [new file with mode: 0644]
mod/quiz/yui/src/repaginate/meta/repaginate.json [new file with mode: 0644]
mod/quiz/yui/src/toolboxes/build.json [new file with mode: 0644]
mod/quiz/yui/src/toolboxes/js/resource.js [new file with mode: 0644]
mod/quiz/yui/src/toolboxes/js/section.js [new file with mode: 0644]
mod/quiz/yui/src/toolboxes/js/toolbox.js [new file with mode: 0644]
mod/quiz/yui/src/toolboxes/meta/toolboxes.json [new file with mode: 0644]
mod/quiz/yui/src/util/build.json [new file with mode: 0644]
mod/quiz/yui/src/util/js/base.js [new file with mode: 0644]
mod/quiz/yui/src/util/js/page.js [new file with mode: 0644]
mod/quiz/yui/src/util/js/slot.js [new file with mode: 0644]
mod/quiz/yui/src/util/meta/util.json [new file with mode: 0644]
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
mod/workshop/locallib.php
mod/workshop/tests/generator/lib.php
mod/workshop/tests/generator_test.php
mod/workshop/tests/locallib_test.php
mod/workshop/version.php
phpunit.xml.dist
pix/e/insert_page_break.png [new file with mode: 0644]
pix/e/insert_page_break.svg [new file with mode: 0644]
pix/e/remove_page_break.png [new file with mode: 0644]
pix/e/remove_page_break.svg [new file with mode: 0644]
pix/t/reset.png [new file with mode: 0644]
pix/t/reset.svg [new file with mode: 0644]
question/addquestion.php
question/classes/bank/checkbox_column.php
question/classes/bank/view.php
question/editlib.php
question/tests/behat/delete_questions.feature
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js
question/yui/src/qbankmanager/js/qbankmanager.js
theme/base/style/grade.css
theme/bootstrapbase/less/moodle/grade.less
theme/bootstrapbase/style/moodle.css
version.php
webservice/lib.php

index b1cfc26..38ec017 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="2.8" requires="2.2">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="5.5.31" />
+      <VENDOR name="mysql" version="5.5.31" />
+      <VENDOR name="postgres" version="9.1" />
+      <VENDOR name="mssql" version="10.0" />
+      <VENDOR name="oracle" version="10.2" />
+    </DATABASE>
+    <PHP version="5.4.4" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opensslrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="intlrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="quizattemptsupgradedmessage" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index 71664a1..ce60366 100644 (file)
@@ -48,11 +48,12 @@ class core_role_potential_assignees_below_course extends core_role_assign_user_s
         $fields      = 'SELECT ' . $this->required_fields_sql('u');
         $countfields = 'SELECT COUNT(u.id)';
 
-        $sql   = " FROM {user} u
-              LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.roleid = :roleid AND ra.contextid = :contextid)
-                  WHERE u.id IN ($enrolsql)
-                        $wherecondition
-                        AND ra.id IS NULL";
+        $sql   = " FROM ($enrolsql) enrolled_users_view
+                   JOIN {user} u ON u.id = enrolled_users_view.id
+              LEFT JOIN {role_assignments} ra ON (ra.userid = enrolled_users_view.id AND
+                                            ra.roleid = :roleid AND ra.contextid = :contextid)
+                  WHERE ra.id IS NULL
+                        $wherecondition";
         $params['contextid'] = $this->context->id;
         $params['roleid'] = $this->roleid;
 
index 2cb6122..84a5296 100644 (file)
@@ -96,11 +96,9 @@ if (has_capability('moodle/grade:manage', $systemcontext)
                          GRADE_AGGREGATE_MODE            =>new lang_string('aggregatemode', 'grades'),
                          GRADE_AGGREGATE_SUM             =>new lang_string('aggregatesum', 'grades'));
 
-        $defaultvisible = array(GRADE_AGGREGATE_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2,
-                                GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_MEDIAN, GRADE_AGGREGATE_MIN,
-                                GRADE_AGGREGATE_MAX, GRADE_AGGREGATE_MODE, GRADE_AGGREGATE_SUM);
+        $defaultvisible = array(GRADE_AGGREGATE_SUM);
 
-        $defaults = array('value'=>GRADE_AGGREGATE_WEIGHTED_MEAN2, 'forced'=>false, 'adv'=>false);
+        $defaults = array('value' => GRADE_AGGREGATE_SUM, 'forced' => false, 'adv' => false);
         $temp->add(new admin_setting_gradecat_combo('grade_aggregation', new lang_string('aggregation', 'grades'), new lang_string('aggregation_help', 'grades'), $defaults, $options));
 
         $temp->add(new admin_setting_configmultiselect('grade_aggregations_visible', new lang_string('aggregationsvisible', 'grades'),
diff --git a/admin/tool/langimport/classes/controller.php b/admin/tool/langimport/classes/controller.php
new file mode 100644 (file)
index 0000000..06f815e
--- /dev/null
@@ -0,0 +1,237 @@
+<?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/>.
+
+/**
+ * Lang import controller
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport;
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir.'/filelib.php');
+require_once($CFG->libdir.'/componentlib.class.php');
+
+/**
+ * Lang import controller
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class controller {
+    /** @var array list of informational messages */
+    public $info;
+    /** @var array  list of error messages */
+    public $errors;
+    /** @var \lang_installer */
+    private $installer;
+    /** @var array languages available on the remote server */
+    public $availablelangs;
+
+    /**
+     * Constructor.
+     */
+    public function __construct() {
+        make_temp_directory('');
+        make_upload_directory('lang');
+
+        $this->info = array();
+        $this->errors = array();
+        $this->installer = new \lang_installer();
+
+        $this->availablelangs = $this->installer->get_remote_list_of_languages();
+    }
+
+    /**
+     * Install language packs provided
+     *
+     * @param string|array $langs array of langcodes or individual langcodes
+     * @param bool $updating true if updating the langpacks
+     * @return int false if an error encountered or
+     * @throws \moodle_exception when error is encountered installing langpack
+     */
+    public function install_languagepacks($langs, $updating = false) {
+        global $CFG;
+
+        $this->installer->set_queue($langs);
+        $results = $this->installer->run();
+
+        $updatedpacks = 0;
+
+        foreach ($results as $langcode => $langstatus) {
+            switch ($langstatus) {
+                case \lang_installer::RESULT_DOWNLOADERROR:
+                    $a       = new \stdClass();
+                    $a->url  = $this->installer->lang_pack_url($langcode);
+                    $a->dest = $CFG->dataroot.'/lang';
+                    $this->errors[] = get_string('remotedownloaderror', 'error', $a);
+                    throw new \moodle_exception('remotedownloaderror', 'error', $a);
+                    break;
+                case \lang_installer::RESULT_INSTALLED:
+                    $updatedpacks++;
+                    if ($updating) {
+                        event\langpack_updated::event_with_langcode($langcode)->trigger();
+                        $this->info[] = get_string('langpackupdated', 'tool_langimport', $langcode);
+                    } else {
+                        $this->info[] = get_string('langpackinstalled', 'tool_langimport', $langcode);
+                        event\langpack_imported::event_with_langcode($langcode)->trigger();
+                    }
+                    break;
+                case \lang_installer::RESULT_UPTODATE:
+                    $this->info[] = get_string('langpackuptodate', 'tool_langimport', $langcode);
+                    break;
+            }
+        }
+
+        return $updatedpacks;
+    }
+
+    /**
+     * Uninstall language pack
+     *
+     * @param string $lang language code
+     * @return bool true if language succesfull installed
+     */
+    public function uninstall_language($lang) {
+        global $CFG;
+
+        $dest1 = $CFG->dataroot.'/lang/'.$lang;
+        $dest2 = $CFG->dirroot.'/lang/'.$lang;
+        $rm1 = false;
+        $rm2 = false;
+        if (file_exists($dest1)) {
+            $rm1 = remove_dir($dest1);
+        }
+        if (file_exists($dest2)) {
+            $rm2 = remove_dir($dest2);
+        }
+
+        if ($rm1 or $rm2) {
+            $this->info[] = get_string('langpackremoved', 'tool_langimport', $lang);
+            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';
+            return false;
+        }
+    }
+
+    /**
+     * Updated all install language packs with the latest found on servre
+     *
+     * @return bool true if languages succesfully updated.
+     */
+    public function update_all_installed_languages() {
+        global $CFG;
+
+        if (!$availablelangs = $this->installer->get_remote_list_of_languages()) {
+            $this->errors[] = get_string('cannotdownloadlanguageupdatelist', 'error');
+            return false;
+        }
+
+        $md5array = array();    // Convert to (string)langcode => (string)md5.
+        foreach ($availablelangs as $alang) {
+            $md5array[$alang[0]] = $alang[1];
+        }
+
+        // Filter out unofficial packs.
+        $currentlangs = array_keys(get_string_manager()->get_list_of_translations(true));
+        $updateablelangs = array();
+        foreach ($currentlangs as $clang) {
+            if (!array_key_exists($clang, $md5array)) {
+                $noticeok[] = get_string('langpackupdateskipped', 'tool_langimport', $clang);
+                continue;
+            }
+            $dest1 = $CFG->dataroot.'/lang/'.$clang;
+            $dest2 = $CFG->dirroot.'/lang/'.$clang;
+
+            if (file_exists($dest1.'/langconfig.php') || file_exists($dest2.'/langconfig.php')) {
+                $updateablelangs[] = $clang;
+            }
+        }
+
+        // Filter out packs that have the same md5 key.
+        $neededlangs = array();
+        foreach ($updateablelangs as $ulang) {
+            if (!$this->is_installed_lang($ulang, $md5array[$ulang])) {
+                $neededlangs[] = $ulang;
+            }
+        }
+
+        // Clean-up currently installed versions of the packs.
+        foreach ($neededlangs as $packindex => $pack) {
+            if ($pack == 'en') {
+                continue;
+            }
+
+            // Delete old directories.
+            $dest1 = $CFG->dataroot.'/lang/'.$pack;
+            $dest2 = $CFG->dirroot.'/lang/'.$pack;
+            if (file_exists($dest1)) {
+                if (!remove_dir($dest1)) {
+                    $noticeerror[] = 'Could not delete old directory '.$dest1.', update of '.$pack
+                        .' failed, please check permissions.';
+                    unset($neededlangs[$packindex]);
+                    continue;
+                }
+            }
+            if (file_exists($dest2)) {
+                if (!remove_dir($dest2)) {
+                    $noticeerror[] = 'Could not delete old directory '.$dest2.', update of '.$pack
+                        .' failed, please check permissions.';
+                    unset($neededlangs[$packindex]);
+                    continue;
+                }
+            }
+        }
+
+        try {
+            $updated = $this->install_languagepacks($neededlangs, true);
+        } catch (\moodle_exception $e) {
+            return false;
+        }
+
+        if ($updated) {
+            $this->info[] = get_string('langupdatecomplete', 'tool_langimport');
+        } else {
+            $this->info[] = get_string('nolangupdateneeded', 'tool_langimport');
+        }
+
+        return true;
+    }
+
+    /**
+     * checks the md5 of the zip file, grabbed from download.moodle.org,
+     * against the md5 of the local language file from last update
+     * @param string $lang language code
+     * @param string $md5check md5 to check
+     * @return bool true if installed
+     */
+    public function is_installed_lang($lang, $md5check) {
+        global $CFG;
+        $md5file = $CFG->dataroot.'/lang/'.$lang.'/'.$lang.'.md5';
+        if (file_exists($md5file)) {
+            return (file_get_contents($md5file) == $md5check);
+        }
+        return false;
+    }
+}
+
+
diff --git a/admin/tool/langimport/classes/event/langpack_imported.php b/admin/tool/langimport/classes/event/langpack_imported.php
new file mode 100644 (file)
index 0000000..c8bd149
--- /dev/null
@@ -0,0 +1,114 @@
+<?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/>.
+
+/**
+ * The langimport langpack imported event.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_langimport langpack imported event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string langcode: the langpage pack code.
+ * }
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class langpack_imported extends \core\event\base {
+    /**
+     * Create instance of event.
+     *
+     * @param string $langcode
+     * @return langpack_updated
+     */
+    public static function event_with_langcode($langcode) {
+        $data = array(
+            'context' => \context_system::instance(),
+            'other' => array(
+                'langcode' => $langcode,
+            )
+        );
+
+        return self::create($data);
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The language pack '{$this->other['langcode']}' was installed.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('langpackinstalledevent', 'tool_langimport');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/tool/langimport/');
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['langcode'])) {
+            throw new \coding_exception('The \'langcode\' value must be set');
+        }
+        // We can't use PARAM_LANG here as the string manager might not be aware of langpack yet.
+        $cleanedlang = clean_param($this->other['langcode'], PARAM_SAFEDIR);
+        if ($cleanedlang !== $this->other['langcode']) {
+            throw new \coding_exception('The \'langcode\' value must be set to a valid language code');
+        }
+    }
+}
diff --git a/admin/tool/langimport/classes/event/langpack_removed.php b/admin/tool/langimport/classes/event/langpack_removed.php
new file mode 100644 (file)
index 0000000..82e5270
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * The langimport langpack removed event.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_langimport langpack removed event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string langcode: the langpage pack code.
+ * }
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class langpack_removed extends \core\event\base {
+    /**
+     * Create instance of event.
+     *
+     * @param string $langcode
+     * @return langpack_updated
+     */
+    public static function event_with_langcode($langcode) {
+        $data = array(
+            'context' => \context_system::instance(),
+            'other' => array(
+                'langcode' => $langcode,
+            )
+        );
+
+        return self::create($data);
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The language pack '{$this->other['langcode']}' was removed.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/tool/langimport/');
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('langpackremovedevent', 'tool_langimport');
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['langcode'])) {
+            throw new \coding_exception('The \'langcode\' value must be set');
+        }
+
+        // We can't use PARAM_LANG here as it queries installed strings.
+        $cleanedlang = clean_param($this->other['langcode'], PARAM_SAFEDIR);
+        if ($cleanedlang !== $this->other['langcode']) {
+            throw new \coding_exception('The \'langcode\' value must be set to a valid language code');
+        }
+    }
+}
diff --git a/admin/tool/langimport/classes/event/langpack_updated.php b/admin/tool/langimport/classes/event/langpack_updated.php
new file mode 100644 (file)
index 0000000..4f0528f
--- /dev/null
@@ -0,0 +1,114 @@
+<?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/>.
+
+/**
+ * The langimport langpack updated event.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_langimport langpack updated event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string langcode: the langpage pack code.
+ * }
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class langpack_updated extends \core\event\base {
+    /**
+     * Create instance of event.
+     *
+     * @param string $langcode
+     * @return langpack_updated
+     */
+    public static function event_with_langcode($langcode) {
+        $data = array(
+            'context' => \context_system::instance(),
+            'other' => array(
+                'langcode' => $langcode,
+            )
+        );
+
+        return self::create($data);
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The language pack '{$this->other['langcode']}' was updated.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('langpackupdatedevent', 'tool_langimport');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/tool/langimport/');
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['langcode'])) {
+            throw new \coding_exception('The \'langcode\' value must be set');
+        }
+
+        $cleanedlang = clean_param($this->other['langcode'], PARAM_LANG);
+        if ($cleanedlang !== $this->other['langcode']) {
+            throw new \coding_exception('The \'langcode\' value must be set to a valid language code');
+        }
+    }
+}
diff --git a/admin/tool/langimport/classes/task/update_langpacks_task.php b/admin/tool/langimport/classes/task/update_langpacks_task.php
new file mode 100644 (file)
index 0000000..68b9e32
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A scheduled task for updating langpacks.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_langimport\task;
+
+/**
+ * A scheduled task for updating langpacks.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class update_langpacks_task extends \core\task\scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('updatelangs', 'tool_langimport');
+    }
+
+    /**
+     * Run langpack update
+     */
+    public function execute() {
+        $controller = new \tool_langimport\controller();
+        if ($controller->update_all_installed_languages()) {
+            foreach ($controller->info as $message) {
+                mtrace($message);
+            }
+            return true;
+        } else {
+            foreach ($controller->errors as $message) {
+                mtrace($message);
+            }
+            return false;
+        }
+
+    }
+
+}
diff --git a/admin/tool/langimport/db/tasks.php b/admin/tool/langimport/db/tasks.php
new file mode 100644 (file)
index 0000000..bd79f1b
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Definition of langimport tasks
+ *
+ * @package   tool_langimport
+ * @category  task
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = array(
+    array(
+        'classname' => 'tool_langimport\task\update_langpacks_task',
+        'blocking' => 0,
+        'minute' => 'R',
+        'hour' => '4',
+        'day' => '*',
+        'month' => '*',
+        'dayofweek' => '*'
+    )
+);
index 01d7a5d..b49dbd3 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-require(dirname(dirname(dirname(dirname(__FILE__)))) . '/config.php');
+require(__DIR__.'/../../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/filelib.php');
-require_once($CFG->libdir.'/componentlib.class.php');
 
 admin_externalpage_setup('toollangimport');
 
@@ -65,37 +63,17 @@ define('UPDATE_ALL_LANG', 5);
 
 get_string_manager()->reset_caches();
 
-$notice_ok    = array();
-$notice_error = array();
+$controller = new tool_langimport\controller();
 
 if (($mode == INSTALLATION_OF_SELECTED_LANG) and confirm_sesskey() and !empty($pack)) {
     core_php_time_limit::raise();
-    make_temp_directory('');
-    make_upload_directory('lang');
-
-    $installer = new lang_installer($pack);
-    $results = $installer->run();
-    foreach ($results as $langcode => $langstatus) {
-        switch ($langstatus) {
-        case lang_installer::RESULT_DOWNLOADERROR:
-            $a       = new stdClass();
-            $a->url  = $installer->lang_pack_url($langcode);
-            $a->dest = $CFG->dataroot.'/lang';
-            print_error('remotedownloaderror', 'error', 'index.php', $a);
-            break;
-        case lang_installer::RESULT_INSTALLED:
-            $notice_ok[] = get_string('langpackinstalled', 'tool_langimport', $langcode);
-            break;
-        case lang_installer::RESULT_UPTODATE:
-            $notice_ok[] = get_string('langpackuptodate', 'tool_langimport', $langcode);
-            break;
-        }
-    }
+    $controller->install_languagepacks($pack);
 }
 
 if ($mode == DELETION_OF_SELECTED_LANG and !empty($uninstalllang)) {
     if ($uninstalllang == 'en') {
-        $notice_error[] = 'English language pack can not be uninstalled';
+        // TODO.
+        $controller->errors[] = 'English language pack can not be uninstalled';
 
     } else if (!$confirm and confirm_sesskey()) {
         echo $OUTPUT->header();
@@ -106,120 +84,13 @@ if ($mode == DELETION_OF_SELECTED_LANG and !empty($uninstalllang)) {
         die;
 
     } else if (confirm_sesskey()) {
-        $dest1 = $CFG->dataroot.'/lang/'.$uninstalllang;
-        $dest2 = $CFG->dirroot.'/lang/'.$uninstalllang;
-        $rm1 = false;
-        $rm2 = false;
-        if (file_exists($dest1)){
-            $rm1 = remove_dir($dest1);
-        }
-        if (file_exists($dest2)){
-            $rm2 = remove_dir($dest2);
-        }
-        if ($rm1 or $rm2) {
-            $notice_ok[] = get_string('langpackremoved', 'tool_langimport');
-        } else {    //nothing deleted, possibly due to permission error
-            $notice_error[] = 'An error has occurred, language pack is not completely uninstalled, please check file permissions';
-        }
+        $controller->uninstall_language($uninstalllang);
     }
 }
 
 if ($mode == UPDATE_ALL_LANG) {
     core_php_time_limit::raise();
-
-    $installer = new lang_installer();
-
-    if (!$availablelangs = $installer->get_remote_list_of_languages()) {
-        print_error('cannotdownloadlanguageupdatelist', 'error');
-    }
-    $md5array = array();    // (string)langcode => (string)md5
-    foreach ($availablelangs as $alang) {
-        $md5array[$alang[0]] = $alang[1];
-    }
-
-    // filter out unofficial packs
-    $currentlangs = array_keys(get_string_manager()->get_list_of_translations(true));
-    $updateablelangs = array();
-    foreach ($currentlangs as $clang) {
-        if (!array_key_exists($clang, $md5array)) {
-            $notice_ok[] = get_string('langpackupdateskipped', 'tool_langimport', $clang);
-            continue;
-        }
-        $dest1 = $CFG->dataroot.'/lang/'.$clang;
-        $dest2 = $CFG->dirroot.'/lang/'.$clang;
-
-        if (file_exists($dest1.'/langconfig.php') || file_exists($dest2.'/langconfig.php')){
-            $updateablelangs[] = $clang;
-        }
-    }
-
-    // then filter out packs that have the same md5 key
-    $neededlangs = array();   // all the packs that needs updating
-    foreach ($updateablelangs as $ulang) {
-        if (!is_installed_lang($ulang, $md5array[$ulang])) {
-            $neededlangs[] = $ulang;
-        }
-    }
-
-    make_temp_directory('');
-    make_upload_directory('lang');
-
-    // clean-up currently installed versions of the packs
-    foreach ($neededlangs as $packindex => $pack) {
-        if ($pack == 'en') {
-            continue;
-        }
-
-        // delete old directories
-        $dest1 = $CFG->dataroot.'/lang/'.$pack;
-        $dest2 = $CFG->dirroot.'/lang/'.$pack;
-        $rm1 = false;
-        $rm2 = false;
-        if (file_exists($dest1)) {
-            if (!remove_dir($dest1)) {
-                $notice_error[] = 'Could not delete old directory '.$dest1.', update of '.$pack.' failed, please check permissions.';
-                unset($neededlangs[$packindex]);
-                continue;
-            }
-        }
-        if (file_exists($dest2)) {
-            if (!remove_dir($dest2)) {
-                $notice_error[] = 'Could not delete old directory '.$dest2.', update of '.$pack.' failed, please check permissions.';
-                unset($neededlangs[$packindex]);
-                continue;
-            }
-        }
-    }
-
-    // install all needed language packs
-    $installer->set_queue($neededlangs);
-    $results = $installer->run();
-    $updated = false;    // any packs updated?
-    foreach ($results as $langcode => $langstatus) {
-        switch ($langstatus) {
-        case lang_installer::RESULT_DOWNLOADERROR:
-            $a       = new stdClass();
-            $a->url  = $installer->lang_pack_url($langcode);
-            $a->dest = $CFG->dataroot.'/lang';
-            print_error('remotedownloaderror', 'error', 'index.php', $a);
-            break;
-        case lang_installer::RESULT_INSTALLED:
-            $updated = true;
-            $notice_ok[] = get_string('langpackinstalled', 'tool_langimport', $langcode);
-            break;
-        case lang_installer::RESULT_UPTODATE:
-            $notice_ok[] = get_string('langpackuptodate', 'tool_langimport', $langcode);
-            break;
-        }
-    }
-
-    if ($updated) {
-        $notice_ok[] = get_string('langupdatecomplete', 'tool_langimport');
-    } else {
-        $notice_ok[] = get_string('nolangupdateneeded', 'tool_langimport');
-    }
-
-    unset($installer);
+    $controller->update_all_installed_languages();
 }
 get_string_manager()->reset_caches();
 
@@ -239,9 +110,7 @@ foreach ($installedlangs as $installedlang => $unused) {
     }
 }
 
-$installer = new lang_installer();
-
-if ($availablelangs = $installer->get_remote_list_of_languages()) {
+if ($availablelangs = $controller->availablelangs) {
     $remote = true;
 } else {
     $remote = false;
@@ -251,18 +120,18 @@ if ($availablelangs = $installer->get_remote_list_of_languages()) {
     echo $OUTPUT->box_end();
 }
 
-if ($notice_ok) {
-    $info = implode('<br />', $notice_ok);
+if ($controller->info) {
+    $info = implode('<br />', $controller->info);
     echo $OUTPUT->notification($info, 'notifysuccess');
 }
 
-if ($notice_error) {
-    $info = implode('<br />', $notice_error);
+if ($controller->errors) {
+    $info = implode('<br />', $controller->errors);
     echo $OUTPUT->notification($info, 'notifyproblem');
 }
 
 if ($missingparents) {
-    foreach ($missingparents as $l=>$parent) {
+    foreach ($missingparents as $l => $parent) {
         $a = new stdClass();
         $a->lang   = $installedlangs[$l];
         $a->parent = $parent;
@@ -306,7 +175,7 @@ echo html_writer::end_tag('td');
 // list of available languages
 $options = array();
 foreach ($availablelangs as $alang) {
-    if (!empty($alang[0]) and trim($alang[0]) !== 'en' and !is_installed_lang($alang[0], $alang[1])) {
+    if (!empty($alang[0]) and trim($alang[0]) !== 'en' and !$controller->is_installed_lang($alang[0], $alang[1])) {
         $options[$alang[0]] = $alang[2].' ('.$alang[0].')';
     }
 }
@@ -331,23 +200,3 @@ echo html_writer::end_tag('table');
 echo $OUTPUT->box_end();
 echo $OUTPUT->footer();
 die();
-
-////////////////////////////////////////////////////////////////////////////////
-// Local functions /////////////////////////////////////////////////////////////
-////////////////////////////////////////////////////////////////////////////////
-
-/**
- * checks the md5 of the zip file, grabbed from download.moodle.org,
- * against the md5 of the local language file from last update
- * @param string $lang
- * @param string $md5check
- * @return bool
- */
-function is_installed_lang($lang, $md5check) {
-    global $CFG;
-    $md5file = $CFG->dataroot.'/lang/'.$lang.'/'.$lang.'.md5';
-    if (file_exists($md5file)){
-        return (file_get_contents($md5file) == $md5check);
-    }
-    return false;
-}
index d7534fb..fba12f9 100644 (file)
@@ -27,10 +27,14 @@ $string['install'] = 'Install selected language pack(s)';
 $string['installedlangs'] = 'Installed language packs';
 $string['langimport'] = 'Language import utility';
 $string['langimportdisabled'] = 'Language import feature has been disabled. You have to update your language packs manually at the file-system level. Do not forget to purge string caches after you do so.';
-$string['langpackinstalled'] = 'Language pack {$a} was successfully installed';
-$string['langpackremoved'] = 'Language pack was uninstalled';
-$string['langpackupdateskipped'] = 'Update of {$a} language pack skipped';
-$string['langpackuptodate'] = 'Language pack {$a} is up-to-date';
+$string['langpackinstalled'] = 'Language pack \'{$a}\' was successfully installed';
+$string['langpackinstalledevent'] = 'Language pack installed';
+$string['langpackremoved'] = 'Language pack \'{$a}\' was uninstalled';
+$string['langpackremovedevent'] = 'Language pack uninstalled';
+$string['langpackupdateskipped'] = 'Update of \'{$a}\' language pack skipped';
+$string['langpackuptodate'] = 'Language pack \'{$a}\' is up-to-date';
+$string['langpackupdated'] = 'Language pack \'{$a}\' was successfully updated';
+$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>.';
diff --git a/admin/tool/langimport/tests/behat/behat_tool_langimport.php b/admin/tool/langimport/tests/behat/behat_tool_langimport.php
new file mode 100644 (file)
index 0000000..f2b5729
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Behat steps definitions for Language import tool
+ *
+ * @package   tool_langimport
+ * @category  test
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+
+use Moodle\BehatExtension\Exception\SkippedException;
+
+/**
+ * Steps definitions related with the Language import tool
+ *
+ * @package   tool_langimport
+ * @category  test
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_tool_langimport extends behat_base {
+
+    /**
+     * This step looks to see if the remote language import tests should be run (indicated by
+     * setting TOOL_LANGIMPORT_REMOTE_TESTS in config.php.
+     *
+     * @Given /^remote langimport tests are enabled$/
+     */
+    public function remote_langimport_tests_are_enabled() {
+        if (!defined('TOOL_LANGIMPORT_REMOTE_TESTS')) {
+            throw new SkippedException('To run the remote langimport tests you must '.
+                'define TOOL_LANGIMPORT_REMOTE_TESTS in config.php');
+        }
+    }
+
+    /**
+     * Downloads a langpack and fakes it being outdated
+     *
+     * @param string $langcode The language code (e.g. en)
+     * @Given /^outdated langpack \'([^\']*)\' is installed$/
+     */
+    public function outdated_langpack_is_installed($langcode) {
+        global $CFG;
+        require_once($CFG->libdir.'/componentlib.class.php');
+
+        // Download the langpack.
+        $dir = make_upload_directory('lang');
+        $installer = new lang_installer($langcode);
+        $result = $installer->run();
+
+        if ($result[$langcode] !== lang_installer::RESULT_INSTALLED) {
+            throw new coding_exception("Failed to install langpack '$langcode'");
+        }
+
+        $path = "$dir/$langcode/$langcode.md5";
+
+        if (!file_exists($path)) {
+            throw new coding_exception("Failed to find '$langcode' checksum");
+        }
+        file_put_contents($path, '000000');
+    }
+}
diff --git a/admin/tool/langimport/tests/behat/manage_langpacks.feature b/admin/tool/langimport/tests/behat/manage_langpacks.feature
new file mode 100644 (file)
index 0000000..ff21c8e
--- /dev/null
@@ -0,0 +1,59 @@
+@tool @tool_langimport
+Feature: Manage language packs
+  In order to support different languages
+  As an administrator
+  I need to be able to add, update and remove language packs
+
+
+  Background:
+    Given remote langimport tests are enabled
+
+  # The pirate language pack is used for testing because its small to download.
+
+  Scenario: Install language pack
+    Given I log in as "admin"
+    And I navigate to "Language packs" node in "Site administration > Language"
+    When I set the field "Available language packs" to "English - Pirate (en_ar)"
+    And I press "Install selected language pack(s)"
+    Then I should see "Language pack 'en_ar' was successfully installed"
+    And the "Installed language packs" select box should contain "English - Pirate (en_ar)"
+    And I navigate to "Live logs" node in "Site administration > Reports"
+    And I should see "The language pack 'en_ar' was installed."
+    And I log out
+
+  Scenario: Update language pack
+    Given outdated langpack 'en_ar' is installed
+    And I log in as "admin"
+    And I navigate to "Language packs" node in "Site administration > Language"
+    When I press "Update all installed language packs"
+    Then I should see "Language pack 'en_ar' was successfully updated"
+    And I should see "Language pack update completed"
+    And I navigate to "Live logs" node in "Site administration > Reports"
+    And I should see "The language pack 'en_ar' was updated."
+    And I log out
+
+  Scenario: Try to uninstall language pack
+    Given I log in as "admin"
+    And I navigate to "Language packs" node in "Site administration > Language"
+    And I set the field "Available language packs" to "English - Pirate (en_ar)"
+    And I press "Install selected language pack(s)"
+    When I set the field "Installed language packs" to "English - Pirate (en_ar)"
+    And I press "Uninstall selected language pack"
+    And I press "Continue"
+    Then I should see "Language pack 'en_ar' was uninstalled"
+    And the "Installed language packs" select box should not contain "English - Pirate (en_ar)"
+    And the "Available language packs" select box should contain "English - Pirate (en_ar)"
+    And I navigate to "Live logs" node in "Site administration > Reports"
+    And I should see "The language pack 'en_ar' was removed."
+    And I should see "Language pack uninstalled"
+    And I log out
+
+  Scenario: Try to uninstall English language pack
+    Given I log in as "admin"
+    And I navigate to "Language packs" node in "Site administration > Language"
+    When I set the field "Installed language packs" to "English (en)"
+    And I press "Uninstall selected language pack"
+    Then I should see "English language pack can not be uninstalled"
+    And I navigate to "Live logs" node in "Site administration > Reports"
+    And I should not see "Language pack uninstalled"
+    And I log out
diff --git a/admin/tool/langimport/tests/events_test.php b/admin/tool/langimport/tests/events_test.php
new file mode 100644 (file)
index 0000000..0a75b34
--- /dev/null
@@ -0,0 +1,102 @@
+<?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/>.
+
+/**
+ * Tests for langimport events.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Test class for langimport events.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
+ */
+class tool_langimport_events_testcase extends advanced_testcase {
+
+    /**
+     * Setup testcase.
+     */
+    public function setUp() {
+        $this->setAdminUser();
+        $this->resetAfterTest();
+    }
+
+    public function test_langpack_updated() {
+        global $CFG;
+
+        $event = \tool_langimport\event\langpack_updated::event_with_langcode($CFG->lang);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        $this->assertInstanceOf('\tool_langimport\event\langpack_updated', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+    }
+
+    public function test_langpack_updated_validation() {
+        $this->setExpectedException('coding_exception', 'The \'langcode\' value must be set to a valid language code');
+
+        \tool_langimport\event\langpack_updated::event_with_langcode('broken langcode');
+    }
+
+    public function test_langpack_installed() {
+        $event = \tool_langimport\event\langpack_imported::event_with_langcode('fr');
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        $this->assertInstanceOf('\tool_langimport\event\langpack_imported', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+    }
+
+    public function test_langpack_installed_validation() {
+        $this->setExpectedException('coding_exception', 'The \'langcode\' value must be set to a valid language code');
+
+        \tool_langimport\event\langpack_imported::event_with_langcode('broken langcode');
+    }
+
+    public function test_langpack_removed() {
+        $event = \tool_langimport\event\langpack_removed::event_with_langcode('fr');
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        $this->assertInstanceOf('\tool_langimport\event\langpack_removed', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+    }
+
+    public function test_langpack_removed_validation() {
+        $this->setExpectedException('coding_exception', 'The \'langcode\' value must be set to a valid language code');
+
+        \tool_langimport\event\langpack_removed::event_with_langcode('broken langcode');
+    }
+}
index 398cfc3..7050367 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2014092801; // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2014050800; // Requires this Moodle version
 $plugin->component = 'tool_langimport'; // Full name of the plugin (used for diagnostics)
index e79c667..4dd37be 100644 (file)
@@ -940,8 +940,8 @@ class backup_gradebook_structure_step extends backup_structure_step {
             'iteminstance', 'itemnumber', 'iteminfo', 'idnumber',
             'calculation', 'gradetype', 'grademax', 'grademin',
             'scaleid', 'outcomeid', 'gradepass', 'multfactor',
-            'plusfactor', 'aggregationcoef', 'sortorder', 'display',
-            'decimals', 'hidden', 'locked', 'locktime',
+            'plusfactor', 'aggregationcoef', 'aggregationcoef2', 'weightoverride',
+            'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
             'needsupdate', 'timecreated', 'timemodified'));
 
         $grade_grades = new backup_nested_element('grade_grades');
@@ -950,7 +950,8 @@ class backup_gradebook_structure_step extends backup_structure_step {
             'rawscaleid', 'usermodified', 'finalgrade', 'hidden',
             'locked', 'locktime', 'exported', 'overridden',
             'excluded', 'feedback', 'feedbackformat', 'information',
-            'informationformat', 'timecreated', 'timemodified'));
+            'informationformat', 'timecreated', 'timemodified',
+            'aggregationstatus', 'aggregationweight'));
 
         //grade_categories
         $grade_categories = new backup_nested_element('grade_categories');
@@ -2163,8 +2164,8 @@ class backup_activity_grades_structure_step extends backup_structure_step {
             'iteminstance', 'itemnumber', 'iteminfo', 'idnumber',
             'calculation', 'gradetype', 'grademax', 'grademin',
             'scaleid', 'outcomeid', 'gradepass', 'multfactor',
-            'plusfactor', 'aggregationcoef', 'sortorder', 'display',
-            'decimals', 'hidden', 'locked', 'locktime',
+            'plusfactor', 'aggregationcoef', 'aggregationcoef2', 'weightoverride',
+            'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
             'needsupdate', 'timecreated', 'timemodified'));
 
         $grades = new backup_nested_element('grade_grades');
@@ -2174,7 +2175,8 @@ class backup_activity_grades_structure_step extends backup_structure_step {
             'rawscaleid', 'usermodified', 'finalgrade', 'hidden',
             'locked', 'locktime', 'exported', 'overridden',
             'excluded', 'feedback', 'feedbackformat', 'information',
-            'informationformat', 'timecreated', 'timemodified'));
+            'informationformat', 'timecreated', 'timemodified',
+            'aggregationstatus', 'aggregationweight'));
 
         $letters = new backup_nested_element('grade_letters');
 
index 14de49b..f13757b 100644 (file)
@@ -804,8 +804,7 @@ class helper {
         }
 
         list($where, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
-        $params['categoryid'] = $moveto->id;
-        $sql = "SELECT c.id, c.category FROM {course} c WHERE c.id {$where} AND c.category <> :categoryid";
+        $sql = "SELECT c.id, c.category FROM {course} c WHERE c.id {$where}";
         $courses = $DB->get_records_sql($sql, $params);
         $checks = array();
         foreach ($courseids as $id) {
index c48905c..6f02f93 100644 (file)
@@ -786,6 +786,31 @@ class core_course_management_renderer extends plugin_renderer_base {
         return $html;
     }
 
+    /**
+     * Renderers bulk actions that can be performed on courses in search returns
+     *
+     * @return string
+     */
+    public function course_search_bulk_actions() {
+        $html  = html_writer::start_div('course-bulk-actions bulk-actions');
+        $html .= html_writer::div(get_string('coursebulkaction'), 'accesshide', array('tabindex' => '0'));
+        $options = coursecat::make_categories_list('moodle/category:manage');
+        $select = html_writer::select(
+            $options,
+            'movecoursesto',
+            '',
+            array('' => 'choosedots'),
+            array('aria-labelledby' => 'moveselectedcoursesto')
+        );
+        $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('move'));
+        $html .= $this->detail_pair(
+            html_writer::span(get_string('moveselectedcoursesto'), '', array('id' => 'moveselectedcoursesto')),
+            $select . html_writer::empty_tag('input', $submit)
+        );
+        $html .= html_writer::end_div();
+        return $html;
+    }
+
     /**
      * Renderers detailed course information.
      *
@@ -1085,6 +1110,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         }
         $html .= html_writer::end_tag('ul');
         $html .= $this->search_pagination($totalcourses, $page, $perpage, true, $search);
+        $html .= $this->course_search_bulk_actions();
         $html .= html_writer::end_div();
         return $html;
     }
@@ -1173,15 +1199,26 @@ class core_course_management_renderer extends plugin_renderer_base {
             'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
             'data-visible' => $course->visible ? '1' : '0'
         );
-
-        $bulkcourseinput = array('type' => 'checkbox', 'name' => 'bc[]', 'value' => $course->id, 'class' => 'bulk-action-checkbox');
+        $bulkcourseinput = '';
+        if (coursecat::get($course->category)->can_move_courses_out_of()) {
+            $bulkcourseinput = array(
+                'type' => 'checkbox',
+                'name' => 'bc[]',
+                'value' => $course->id,
+                'class' => 'bulk-action-checkbox',
+                'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+                'data-action' => 'select'
+            );
+        }
         $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
         $categoryname = coursecat::get($course->category)->get_formatted_name();
 
         $html  = html_writer::start_tag('li', $attributes);
         $html .= html_writer::start_div('clearfix');
         $html .= html_writer::start_div('float-left');
-        $html .= html_writer::empty_tag('input', $bulkcourseinput).'&nbsp;';
+        if ($bulkcourseinput) {
+            $html .= html_writer::empty_tag('input', $bulkcourseinput).'&nbsp;';
+        }
         $html .= html_writer::end_div();
         $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
         $html .= html_writer::tag('span', $categoryname, array('class' => 'float-left categoryname'));
index db506fb..8c4d6ff 100644 (file)
@@ -290,7 +290,7 @@ if ($action !== false && confirm_sesskey()) {
                 $moveto = coursecat::get($movetoid);
                 try {
                     // If this fails we want to catch the exception and report it.
-                    $redirectback = \core_course\management\helper::action_category_move_courses_into($category, $moveto,
+                    $redirectback = \core_course\management\helper::move_courses_into_category($moveto,
                         $courseids);
                     if ($redirectback) {
                         $a = new stdClass;
index 55d242f..dab59f8 100644 (file)
@@ -1676,13 +1676,12 @@ class core_course_renderer extends plugin_renderer_base {
         $site = get_site();
         $output = '';
 
-        $this->page->set_button($this->course_search_form('', 'navbar'));
+        if (can_edit_in_category($category)) {
+            // Add 'Manage' button if user has permissions to edit this category.
+            $managebutton = $this->single_button(new moodle_url('/course/management.php'), get_string('managecourses'), 'get');
+            $this->page->set_button($managebutton);
+        }
         if (!$coursecat->id) {
-            if (can_edit_in_category()) {
-                // add 'Manage' button instead of course search form
-                $managebutton = $this->single_button(new moodle_url('/course/management.php'), get_string('managecourses'), 'get');
-                $this->page->set_button($managebutton);
-            }
             if (coursecat::count_all() == 1) {
                 // There exists only one category in the system, do not display link to it
                 $coursecat = coursecat::get_default();
@@ -1746,15 +1745,12 @@ class core_course_renderer extends plugin_renderer_base {
             $catdisplayoptions['viewmoreurl'] = new moodle_url($baseurl, array('browse' => 'categories', 'page' => 1));
         }
         $chelper->set_courses_display_options($coursedisplayoptions)->set_categories_display_options($catdisplayoptions);
+        // Add course search form.
+        $output .= $this->course_search_form();
 
-        // Display course category tree
+        // Display course category tree.
         $output .= $this->coursecat_tree($chelper, $coursecat);
 
-        // Add course search form (if we are inside category it was already added to the navbar)
-        if (!$coursecat->id) {
-            $output .= $this->course_search_form();
-        }
-
         // Add action buttons
         $output .= $this->container_start('buttons');
         $context = get_category_or_system_context($coursecat->id);
index 622909b..52ba8fa 100644 (file)
@@ -1,10 +1,37 @@
 <?php
-if (!defined('MOODLE_INTERNAL')) {
-    die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
-}
+// 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/>.
+
+/**
+ * Provides the course_reset_form class.
+ *
+ * @package     core
+ * @copyright   2007 Petr Skoda
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
 
 require_once $CFG->libdir.'/formslib.php';
 
+/**
+ * Defines the course reset settings form.
+ *
+ * @copyright   2007 Petr Skoda
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 class course_reset_form extends moodleform {
     function definition (){
         global $CFG, $COURSE, $DB;
diff --git a/course/tests/behat/course_search.feature b/course/tests/behat/course_search.feature
new file mode 100644 (file)
index 0000000..24bfaad
--- /dev/null
@@ -0,0 +1,45 @@
+@core @core_course
+Feature: Courses can be searched for and moved in bulk.
+  In order to manage a large number of courses
+  As a Moodle Administrator
+  I need to be able to search courses in bulk and move them around
+
+  Background:
+     Given the following "categories" exist:
+      | name | category | idnumber |
+      | Science | 0 | SCI |
+      | English | 0 | ENG |
+      | Miscellaneous | 0 | MISC |
+
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Biology Y1 | BIO1 | MISC |
+      | Biology Y2 | BIO2 | MISC |
+      | English Y1 | ENG1 | ENG |
+      | English Y2 | ENG2 | MISC |
+
+  Scenario: Search courses finds correct results
+    Given I log in as "admin"
+    And I go to the courses management page
+    When I set the field "Search courses:" to "Biology"
+    And I press "Go"
+    Then I should see "Biology Y1"
+    And I should see "Biology Y2"
+    And I should not see "English Y1"
+    And I should not see "English Y2"
+
+  @javascript
+  Scenario: Search courses and move results in bulk
+    Given I log in as "admin"
+    And I go to the courses management page
+    And I set the field "Search courses:" to "Biology"
+    And I press "Go"
+    When I select course "Biology Y1" in the management interface
+    And I select course "Biology Y2" in the management interface
+    And I set the field "menumovecoursesto" to "Science"
+    And I press "Move"
+    Then I should see "Successfully moved 2 courses into Science"
+    And I wait to be redirected
+    And I click on category "Science" in the management interface
+    And I should see "Biology Y1"
+    And I should see "Biology Y2"
index 04373b1..e016560 100644 (file)
@@ -32,6 +32,8 @@ $id       = optional_param('id', 0, PARAM_INT);
 
 $PAGE->set_url('/grade/edit/scale/edit.php', array('id' => $id, 'courseid' => $courseid));
 $PAGE->set_pagelayout('admin');
+navigation_node::override_active_url(new moodle_url('/grade/edit/scale/index.php',
+    array('id' => $courseid)));
 
 $systemcontext = context_system::instance();
 $heading = '';
@@ -145,11 +147,7 @@ if ($mform->is_cancelled()) {
     redirect($returnurl);
 }
 
-if ($courseid) {
-    print_grade_page_head($course->id, 'scale', 'edit', $heading);
-} else {
-    echo $OUTPUT->header();
-}
+print_grade_page_head($COURSE->id, 'scale', null, $heading, false, false, false);
 
 $mform->display();
 
index fa7a81f..3baacc7 100644 (file)
@@ -45,8 +45,6 @@ $gpr = new grade_plugin_return(array('type'=>'edit', 'plugin'=>'settings', 'cour
 $strgrades = get_string('grades');
 $pagename  = get_string('coursesettings', 'grades');
 
-$navigation = grade_build_nav(__FILE__, $pagename, $courseid);
-
 $returnurl = $CFG->wwwroot.'/grade/index.php?id='.$course->id;
 
 $mform = new course_settings_form();
@@ -76,7 +74,7 @@ if ($mform->is_cancelled()) {
     redirect($returnurl);
 }
 
-print_grade_page_head($courseid, 'settings', 'coursesettings', get_string('coursesettings', 'grades'));
+print_grade_page_head($courseid, 'settings', 'coursesettings', get_string('coursegradesettings', 'grades'));
 
 echo $OUTPUT->box_start('generalbox boxaligncenter boxwidthnormal centerpara');
 echo get_string('coursesettingsexplanation', 'grades');
index f8d18f3..cea8791 100644 (file)
@@ -107,6 +107,37 @@ switch ($action) {
             $object->set_locked(0, true, true);
         }
         break;
+
+    case 'resetweights':
+        if ($eid && confirm_sesskey()) {
+
+            // This is specific to category items with natural weight as an aggregation method, and can
+            // only be done by someone who can manage the grades.
+            if ($type != 'category' || $object->aggregation != GRADE_AGGREGATE_SUM ||
+                    !has_capability('moodle/grade:manage', $context)) {
+                print_error('nopermissiontoresetweights', 'grades', $returnurl);
+            }
+
+            // Remove the weightoverride flag from the children.
+            $children = $object->get_children();
+            foreach ($children as $item) {
+                if ($item['type'] == 'category') {
+                    $gradeitem = $item['object']->load_grade_item();
+                } else {
+                    $gradeitem = $item['object'];
+                }
+
+                if ($gradeitem->weightoverride == false) {
+                    continue;
+                }
+
+                $gradeitem->weightoverride = false;
+                $gradeitem->update();
+            }
+
+            // Force regrading.
+            $object->force_regrading();
+        }
 }
 
 redirect($returnurl);
index bd27551..e0dabcf 100644 (file)
@@ -46,6 +46,10 @@ require_login($course);
 $context = context_course::instance($course->id);
 require_capability('moodle/grade:manage', $context);
 
+$PAGE->set_pagelayout('admin');
+navigation_node::override_active_url(new moodle_url('/grade/edit/tree/index.php',
+    array('id'=>$course->id)));
+
 // default return url
 $gpr = new grade_plugin_return();
 $returnurl = $gpr->get_return_url($CFG->wwwroot.'/grade/report/index.php?id='.$course->id);
@@ -109,10 +113,8 @@ $strgrades          = get_string('grades');
 $strgraderreport    = get_string('graderreport', 'grades');
 $strcalculationedit = get_string('editcalculation', 'grades');
 
-grade_build_nav(__FILE__, $strcalculationedit, array('courseid' => $courseid));
-$PAGE->set_title($strgrades . ': ' . $strgraderreport);
-$PAGE->set_heading($course->fullname);
-echo $OUTPUT->header();
+$PAGE->navbar->add($strcalculationedit);
+print_grade_page_head($courseid, 'settings', null, $strcalculationedit, false, false, false);
 
 $mform->display();
 // Now show the gradetree with the idnumbers add/edit form
index 852fdd5..206b12d 100644 (file)
@@ -36,6 +36,8 @@ if ($id !== 0) {
 }
 $PAGE->set_url($url);
 $PAGE->set_pagelayout('admin');
+navigation_node::override_active_url(new moodle_url('/grade/edit/tree/index.php',
+    array('id'=>$courseid)));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
     print_error('nocourseid');
@@ -75,6 +77,7 @@ if ($id) {
     $category->grade_item_gradepass  = format_float($category->grade_item_gradepass, $decimalpoints);
     $category->grade_item_multfactor = format_float($category->grade_item_multfactor, 4);
     $category->grade_item_plusfactor = format_float($category->grade_item_plusfactor, 4);
+    $category->grade_item_aggregationcoef2 = format_float($category->grade_item_aggregationcoef2 * 100.0, 4);
 
     if (!$parent_category) {
         // keep as is
@@ -84,6 +87,16 @@ if ($id) {
         $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;
+    }
+
 } else {
     $heading = get_string('newcategory', 'grades');
     $grade_category = new grade_category(array('courseid'=>$courseid), false);
@@ -160,12 +173,15 @@ if ($mform->is_cancelled()) {
     unset($itemdata->locked);
     unset($itemdata->locktime);
 
-    $convert = array('grademax', 'grademin', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef');
+    $convert = array('grademax', 'grademin', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef', 'aggregationcoef2');
     foreach ($convert as $param) {
         if (property_exists($itemdata, $param)) {
             $itemdata->$param = unformat_float($itemdata->$param);
         }
     }
+    if (isset($itemdata->aggregationcoef2)) {
+        $itemdata->aggregationcoef2 = $itemdata->aggregationcoef2 / 100.0;
+    }
 
     // When creating a new category, a number of grade item fields are filled out automatically, and are required.
     // If the user leaves these fields empty during creation of a category, we let the default values take effect
@@ -195,6 +211,14 @@ if ($mform->is_cancelled()) {
         $grade_item->decimals = null;
     }
 
+    // Change weightoverride flag. Check if the value is set, because it is not when the checkbox is not ticked.
+    $itemdata->weightoverride = isset($itemdata->weightoverride) ? $itemdata->weightoverride : 0;
+    if ($grade_item->weightoverride != $itemdata->weightoverride && $grade_category->aggregation == GRADE_AGGREGATE_SUM) {
+        // If we are using natural weight and the weight has been un-overriden, force parent category to recalculate weights.
+        $grade_category->force_regrading();
+    }
+    $grade_item->weightoverride = $itemdata->weightoverride;
+
     $grade_item->outcomeid = null;
 
     // update hiding flag
@@ -217,10 +241,8 @@ if ($mform->is_cancelled()) {
     redirect($returnurl);
 }
 
-$return = false;
-$buttons = false;
-$shownavigation = false;
-print_grade_page_head($courseid, 'edittree', null, $heading, $return, $buttons, $shownavigation);
+$PAGE->navbar->add($heading);
+print_grade_page_head($courseid, 'settings', null, $heading, false, false, false);
 
 $mform->display();
 
index f893091..6a155fd 100644 (file)
@@ -37,15 +37,7 @@ class edit_category_form extends moodleform {
 
         $category = $this->_customdata['current'];
 
-        $this->aggregation_options = array(GRADE_AGGREGATE_MEAN            =>get_string('aggregatemean', 'grades'),
-                                           GRADE_AGGREGATE_WEIGHTED_MEAN   =>get_string('aggregateweightedmean', 'grades'),
-                                           GRADE_AGGREGATE_WEIGHTED_MEAN2  =>get_string('aggregateweightedmean2', 'grades'),
-                                           GRADE_AGGREGATE_EXTRACREDIT_MEAN=>get_string('aggregateextracreditmean', 'grades'),
-                                           GRADE_AGGREGATE_MEDIAN          =>get_string('aggregatemedian', 'grades'),
-                                           GRADE_AGGREGATE_MIN             =>get_string('aggregatemin', 'grades'),
-                                           GRADE_AGGREGATE_MAX             =>get_string('aggregatemax', 'grades'),
-                                           GRADE_AGGREGATE_MODE            =>get_string('aggregatemode', 'grades'),
-                                           GRADE_AGGREGATE_SUM             =>get_string('aggregatesum', 'grades'));
+        $this->aggregation_options = grade_helper::get_aggregation_strings();
 
         // visible elements
         $mform->addElement('header', 'headercategory', get_string('gradecategory', 'grades'));
@@ -62,7 +54,6 @@ class edit_category_form extends moodleform {
 
         $mform->addElement('checkbox', 'aggregateonlygraded', get_string('aggregateonlygraded', 'grades'));
         $mform->addHelpButton('aggregateonlygraded', 'aggregateonlygraded', 'grades');
-        $mform->disabledIf('aggregateonlygraded', 'aggregation', 'eq', GRADE_AGGREGATE_SUM);
 
         if ((int)$CFG->grade_aggregateonlygraded_flag & 2) {
             $mform->setAdvanced('aggregateonlygraded');
@@ -170,6 +161,14 @@ class edit_category_form extends moodleform {
             $mform->disabledIf('grade_item_grademin', 'aggregation', 'eq', GRADE_AGGREGATE_SUM);
         }
 
+        $mform->addElement('advcheckbox', 'grade_item_weightoverride', get_string('adjustedweight', 'grades'));
+        $mform->addHelpButton('grade_item_weightoverride', 'weightoverride', 'grades');
+
+        $mform->addElement('text', 'grade_item_aggregationcoef2', get_string('weight', 'grades'));
+        $mform->addHelpButton('grade_item_aggregationcoef2', 'weight', 'grades');
+        $mform->setType('grade_item_aggregationcoef2', PARAM_RAW);
+        $mform->disabledIf('grade_item_aggregationcoef2', 'grade_item_weightoverride');
+
         $mform->addElement('text', 'grade_item_gradepass', get_string('gradepass', 'grades'));
         $mform->setType('grade_item_gradepass', PARAM_RAW);
         $mform->addHelpButton('grade_item_gradepass', 'gradepass', 'grades');
@@ -332,6 +331,18 @@ class edit_category_form extends moodleform {
 
             }
 
+            // Prevent the user from using drop lowest/keep highest when the aggregation method cannot handle it.
+            if (!$grade_category->can_apply_limit_rules()) {
+                if ($mform->elementExists('keephigh')) {
+                    $mform->setConstant('keephigh', 0);
+                    $mform->hardFreeze('keephigh');
+                }
+                if ($mform->elementExists('droplow')) {
+                    $mform->setConstant('droplow', 0);
+                    $mform->hardFreeze('droplow');
+                }
+            }
+
             if ($grade_item->is_calculated()) {
                 // following elements are ignored when calculation formula used
                 if ($mform->elementExists('aggregation')) {
@@ -430,6 +441,12 @@ class edit_category_form extends moodleform {
                     $mform->removeElement('grade_item_aggregationcoef');
                 }
 
+                if ($mform->elementExists('grade_item_weightoverride')) {
+                    $mform->removeElement('grade_item_weightoverride');
+                }
+                if ($mform->elementExists('grade_item_aggregationcoef2')) {
+                    $mform->removeElement('grade_item_aggregationcoef2');
+                }
             } else {
                 if ($grade_item->is_category_item()) {
                     $category = $grade_item->get_item_category();
@@ -448,8 +465,9 @@ class edit_category_form extends moodleform {
 
                     $coefstring = $grade_item->get_coefstring();
 
-                    if ($coefstring == 'aggregationcoefextrasum') {
+                    if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
                         // advcheckbox is not compatible with disabledIf!
+                        $coefstring = 'aggregationcoefextrasum';
                         $element =& $mform->createElement('checkbox', 'grade_item_aggregationcoef', get_string($coefstring, 'grades'));
                     } else {
                         $element =& $mform->createElement('text', 'grade_item_aggregationcoef', get_string($coefstring, 'grades'));
@@ -457,6 +475,16 @@ class edit_category_form extends moodleform {
                     $mform->insertElementBefore($element, 'parentcategory');
                     $mform->addHelpButton('grade_item_aggregationcoef', $coefstring, 'grades');
                 }
+
+                // Remove fields used by natural weighting if the parent category is not using natural weighting.
+                if ($parent_category->aggregation != GRADE_AGGREGATE_SUM) {
+                    if ($mform->elementExists('grade_item_weightoverride')) {
+                        $mform->removeElement('grade_item_weightoverride');
+                    }
+                    if ($mform->elementExists('grade_item_aggregationcoef2')) {
+                        $mform->removeElement('grade_item_aggregationcoef2');
+                    }
+                }
             }
         }
     }
index 5649ea2..2da2f01 100644 (file)
@@ -69,6 +69,18 @@ function update_category_aggregation(e, args) {
     window.location = 'index.php?id='+args.courseid+'&category='+args.category+'&aggregationtype='+selectmenu.get('value')+'&sesskey='+args.sesskey;
 }
 
+/**
+ * The weight override checkboxes toggle the disabled status of their associated weight fields.
+ */
+YUI().use('node', 'delegate', function(Y) {
+    Y.on('domready', function() {
+        Y.delegate('click', function(e) {
+            var t = e.currentTarget,
+                itemid = t.get('id').split('_')[1];
+            Y.one('input[name=weight_' + itemid + ']').set('disabled', t.get('checked') ? false : true);
+        }, Y.config.doc.body, 'input.weightoverride');
+    });
+});
 
 /* TODO: finish and rewrite for YUI3...
 Y.YUI2.namespace('grade_edit_tree');
index 06e5162..be8a0a6 100644 (file)
@@ -85,11 +85,7 @@ class edit_grade_form extends moodleform {
             $mform->disabledIf('finalgrade', 'overridden', 'notchecked');
         }
 
-        if ($grade_category and $grade_category->aggregation == GRADE_AGGREGATE_SUM) {
-            $mform->addElement('advcheckbox', 'excluded', get_string('excluded', 'grades'), '<small>('.get_string('warningexcludedsum', 'grades').')</small>');
-        } else {
-            $mform->addElement('advcheckbox', 'excluded', get_string('excluded', 'grades'));
-        }
+        $mform->addElement('advcheckbox', 'excluded', get_string('excluded', 'grades'));
         $mform->addHelpButton('excluded', 'excluded', 'grades');
 
         /// hiding
index 68bb267..849c23b 100644 (file)
@@ -32,12 +32,8 @@ $action          = optional_param('action', 0, PARAM_ALPHA);
 $eid             = optional_param('eid', 0, PARAM_ALPHANUM);
 $category        = optional_param('category', null, PARAM_INT);
 $aggregationtype = optional_param('aggregationtype', null, PARAM_INT);
-$showadvanced    = optional_param('showadvanced', -1, PARAM_BOOL); // sticky editing mode
 
 $url = new moodle_url('/grade/edit/tree/index.php', array('id' => $courseid));
-if($showadvanced!=-1) {
-    $url->param("showadvanced",$showadvanced);
-}
 $PAGE->set_url($url);
 $PAGE->set_pagelayout('admin');
 
@@ -57,41 +53,6 @@ $PAGE->requires->js('/grade/edit/tree/functions.js');
 $gpr = new grade_plugin_return(array('type'=>'edit', 'plugin'=>'tree', 'courseid'=>$courseid));
 $returnurl = $gpr->get_return_url(null);
 
-/// Build editing on/off buttons
-if (!isset($USER->gradeediting)) {
-    $USER->gradeediting = array();
-}
-
-$current_view = '';
-
-if (has_capability('moodle/grade:manage', $context)) {
-    if (!isset($USER->gradeediting[$course->id])) {
-        $USER->gradeediting[$course->id] = 0;
-    }
-
-    if ($showadvanced == 1) {
-        $USER->gradeediting[$course->id] = 1;
-    } else if ($showadvanced == 0) {
-        $USER->gradeediting[$course->id] = 0;
-    }
-
-    // page params for the turn editing on
-    $options = $gpr->get_options();
-    $options['sesskey'] = sesskey();
-
-    if ($USER->gradeediting[$course->id]) {
-        $options['showadvanced'] = 0;
-        $current_view = 'fullview';
-    } else {
-        $options['showadvanced'] = 1;
-        $current_view = 'simpleview';
-    }
-
-} else {
-    $USER->gradeediting[$course->id] = 0;
-    $buttons = '';
-}
-
 // Change category aggregation if requested
 if (!is_null($category) && !is_null($aggregationtype) && confirm_sesskey()) {
     if (!$grade_category = grade_category::fetch(array('id'=>$category, 'courseid'=>$courseid))) {
@@ -107,8 +68,18 @@ if (!is_null($category) && !is_null($aggregationtype) && confirm_sesskey()) {
 }
 
 //first make sure we have proper final grades - we need it for locking changes
+$normalisationmessage = null;
+
+$originalweights = grade_helper::fetch_all_natural_weights_for_course($courseid);
+
 grade_regrade_final_grades($courseid);
 
+$alteredweights = grade_helper::fetch_all_natural_weights_for_course($courseid);
+
+if (array_diff($originalweights, $alteredweights)) {
+    $normalisationmessage = get_string('weightsadjusted', 'grades');
+}
+
 // get the grading tree object
 // note: total must be first for moving to work correctly, if you want it last moving code must be rewritten!
 $gtree = new grade_tree($courseid, false, false);
@@ -128,8 +99,6 @@ $switch = grade_get_setting($course->id, 'aggregationposition', $CFG->grade_aggr
 
 $strgrades             = get_string('grades');
 $strgraderreport       = get_string('graderreport', 'grades');
-$strcategoriesedit     = get_string('categoriesedit', 'grades');
-$strcategoriesanditems = get_string('categoriesanditems', 'grades');
 
 $moving = false;
 $movingeid = false;
@@ -206,21 +175,6 @@ switch ($action) {
         break;
 }
 
-// Hide advanced columns if moving
-if ($grade_edit_tree->moving) {
-    $original_gradeediting = $USER->gradeediting[$course->id];
-    $USER->gradeediting[$course->id] = 0;
-}
-
-$current_view_str = '';
-if ($current_view != '') {
-    if ($current_view == 'simpleview') {
-        $current_view_str = get_string('simpleview', 'grades');
-    } elseif ($current_view == 'fullview') {
-        $current_view_str = get_string('fullview', 'grades');
-    }
-}
-
 //if we go straight to the db to update an element we need to recreate the tree as
 // $grade_edit_tree has already been constructed.
 //Ideally we could do the updates through $grade_edit_tree to avoid recreating it
@@ -232,7 +186,7 @@ if ($data = data_submitted() and confirm_sesskey()) {
         $elements = array();
 
         foreach ($data as $key => $value) {
-            if (preg_match('/select_(i[0-9]*)/', $key, $matches)) {
+            if (preg_match('/select_(ig[0-9]*)/', $key, $matches)) {
                 $elements[] = $matches[1];
             }
         }
@@ -240,77 +194,57 @@ if ($data = data_submitted() and confirm_sesskey()) {
         $grade_edit_tree->move_elements($elements, $returnurl);
     }
 
-    // Category and item field updates
+    // Update weights (extra credits) on categories and items.
     foreach ($data as $key => $value) {
-        // Grade category text inputs
-        if (preg_match('/^(aggregation|droplow|keephigh)_([0-9]+)$/', $key, $matches)) {
-            $param = $matches[1];
-            $aid   = $matches[2];
-
-            // Do not allow negative values
-            $value = clean_param($value, PARAM_INT);
-            $value = ($value < 0) ? 0 : $value;
-
-            $grade_category = grade_category::fetch(array('id'=>$aid, 'courseid'=>$courseid));
-            $grade_category->$param = $value;
-
-            $grade_category->update();
-            grade_regrade_final_grades($courseid);
-
-            $recreatetree = true;
-
-        // Grade item text inputs
-        } elseif (preg_match('/^(grademax|aggregationcoef|multfactor|plusfactor)_([0-9]+)$/', $key, $matches)) {
-            $param = $matches[1];
-            $aid   = $matches[2];
+        if (preg_match('/^weight_([0-9]+)$/', $key, $matches)) {
+            $aid   = $matches[1];
 
             $value = unformat_float($value);
             $value = clean_param($value, PARAM_FLOAT);
 
-            $grade_item = grade_item::fetch(array('id'=>$aid, 'courseid'=>$courseid));
-
-            if ($param === 'grademax' and $value < $grade_item->grademin) {
-                // better not allow values lower than grade min
-                $value = $grade_item->grademin;
+            $grade_item = grade_item::fetch(array('id' => $aid, 'courseid' => $courseid));
+
+            // Convert weight to aggregation coef2.
+            $aggcoef = $grade_item->get_coefstring();
+            if ($aggcoef == 'aggregationcoefextraweightsum') {
+                // The field 'weight' should only be sent when the checkbox 'weighoverride' is checked,
+                // so there is not need to set weightoverride here, it is done below.
+                $value = $value / 100.0;
+                $grade_item->aggregationcoef2 = $value;
+            } else if ($aggcoef == 'aggregationcoefweight' || $aggcoef == 'aggregationcoefextraweight') {
+                $grade_item->aggregationcoef = $value;
             }
-            $grade_item->$param = $value;
 
             $grade_item->update();
-            grade_regrade_final_grades($courseid);
 
             $recreatetree = true;
 
-        // Grade item checkbox inputs
-        } elseif (preg_match('/^extracredit_([0-9]+)$/', $key, $matches)) { // Sum extra credit checkbox
-            $aid   = $matches[1];
-            $value = clean_param($value, PARAM_BOOL);
+        // Grade item checkbox inputs.
+        } elseif (preg_match('/^(weightoverride)_([0-9]+)$/', $key, $matches)) {
+            $param   = $matches[1];
+            $aid     = $matches[2];
+            $value   = clean_param($value, PARAM_BOOL);
 
-            $grade_item = grade_item::fetch(array('id'=>$aid, 'courseid'=>$courseid));
-            $grade_item->aggregationcoef = $value;
+            $grade_item = grade_item::fetch(array('id' => $aid, 'courseid' => $courseid));
+            $grade_item->$param = $value;
 
             $grade_item->update();
-            grade_regrade_final_grades($courseid);
 
             $recreatetree = true;
+        }
+    }
 
-        // Grade category checkbox inputs
-        } elseif (preg_match('/^aggregate(onlygraded|subcats|outcomes)_([0-9]+)$/', $key, $matches)) {
-            $param = 'aggregate'.$matches[1];
-            $aid    = $matches[2];
-            $value = clean_param($value, PARAM_BOOL);
-
-            $grade_category = grade_category::fetch(array('id'=>$aid, 'courseid'=>$courseid));
-            $grade_category->$param = $value;
+    $originalweights = grade_helper::fetch_all_natural_weights_for_course($courseid);
 
-            $grade_category->update();
-            grade_regrade_final_grades($courseid);
+    grade_regrade_final_grades($courseid);
 
-            $recreatetree = true;
-        }
+    $alteredweights = grade_helper::fetch_all_natural_weights_for_course($courseid);
+    if (array_diff($originalweights, $alteredweights)) {
+        $normalisationmessage = get_string('weightsadjusted', 'grades');
     }
 }
 
-print_grade_page_head($courseid, 'edittree', $current_view, get_string('categoriesedit', 'grades') . ': ' . $current_view_str);
+print_grade_page_head($courseid, 'settings', 'setup', get_string('setupgradeslayout', 'grades'));
 
 // Print Table of categories and items
 echo $OUTPUT->box_start('gradetreebox generalbox');
@@ -323,6 +257,10 @@ echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
 if ($recreatetree) {
     $grade_edit_tree = new grade_edit_tree($gtree, $movingeid, $gpr);
 }
+// Check to see if we have a normalisation message to send.
+if (!empty($normalisationmessage)) {
+    echo $OUTPUT->notification($normalisationmessage, 'notifymessage');
+}
 
 echo html_writer::table($grade_edit_tree->table);
 
@@ -377,11 +315,6 @@ $PAGE->requires->yui_module('moodle-core-formchangechecker',
 $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
 
 echo $OUTPUT->footer();
-
-// Restore original show/hide preference if moving
-if ($moving) {
-    $USER->gradeediting[$course->id] = $original_gradeediting;
-}
 die;
 
 
index 7acf2eb..acbf581 100644 (file)
@@ -37,6 +37,8 @@ if ($id !== 0) {
 }
 $PAGE->set_url($url);
 $PAGE->set_pagelayout('admin');
+navigation_node::override_active_url(new moodle_url('/grade/edit/tree/index.php',
+    array('id'=>$courseid)));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
     print_error('nocourseid');
@@ -97,6 +99,9 @@ if ($parent_category->aggregation == GRADE_AGGREGATE_SUM or $parent_category->ag
 } else {
     $item->aggregationcoef = format_float($item->aggregationcoef, 4);
 }
+if ($parent_category->aggregation == GRADE_AGGREGATE_SUM) {
+    $item->aggregationcoef2 = format_float($item->aggregationcoef2 * 100.0);
+}
 $item->cancontrolvisibility = $grade_item->can_control_visibility();
 
 $mform = new edit_item_form(null, array('current'=>$item, 'gpr'=>$gpr));
@@ -132,12 +137,15 @@ if ($mform->is_cancelled()) {
     unset($data->locked);
     unset($data->locktime);
 
-    $convert = array('grademax', 'grademin', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef');
+    $convert = array('grademax', 'grademin', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef', 'aggregationcoef2');
     foreach ($convert as $param) {
         if (property_exists($data, $param)) {
             $data->$param = unformat_float($data->$param);
         }
     }
+    if (isset($data->aggregationcoef2) && $parent_category->aggregation == GRADE_AGGREGATE_SUM) {
+        $data->aggregationcoef2 = $data->aggregationcoef2 / 100.0;
+    }
 
     $grade_item = new grade_item(array('id'=>$id, 'courseid'=>$courseid));
     grade_item::set_properties($grade_item, $data);
@@ -174,10 +182,8 @@ if ($mform->is_cancelled()) {
     redirect($returnurl);
 }
 
-$return = false;
-$buttons = false;
-$shownavigation = false;
-print_grade_page_head($courseid, 'edittree', null, $heading, $return, $buttons, $shownavigation);
+$PAGE->navbar->add($heading);
+print_grade_page_head($courseid, 'settings', null, $heading, false, false, false);
 
 $mform->display();
 
index 13ad687..a74120f 100644 (file)
@@ -97,6 +97,14 @@ class edit_item_form extends moodleform {
             $mform->setType('grademin', PARAM_RAW);
         }
 
+        $mform->addElement('advcheckbox', 'weightoverride', get_string('adjustedweight', 'grades'));
+        $mform->addHelpButton('weightoverride', 'weightoverride', 'grades');
+
+        $mform->addElement('text', 'aggregationcoef2', get_string('weight', 'grades'));
+        $mform->addHelpButton('aggregationcoef2', 'weight', 'grades');
+        $mform->setType('aggregationcoef2', PARAM_RAW);
+        $mform->disabledIf('aggregationcoef2', 'weightoverride');
+
         $mform->addElement('text', 'gradepass', get_string('gradepass', 'grades'));
         $mform->addHelpButton('gradepass', 'gradepass', 'grades');
         $mform->disabledIf('gradepass', 'gradetype', 'eq', GRADE_TYPE_NONE);
@@ -278,8 +286,9 @@ class edit_item_form extends moodleform {
                 $coefstring = $grade_item->get_coefstring();
 
                 if ($coefstring !== '') {
-                    if ($coefstring == 'aggregationcoefextrasum') {
+                    if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
                         // advcheckbox is not compatible with disabledIf!
+                        $coefstring = 'aggregationcoefextrasum';
                         $element =& $mform->createElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
                     } else {
                         $element =& $mform->createElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
@@ -295,6 +304,16 @@ class edit_item_form extends moodleform {
                 $mform->disabledIf('aggregationcoef', 'parentcategory', 'eq', $parent_category->id);
             }
 
+            // Remove fields used by natural weighting if the parent category is not using natural weighting.
+            if ($parent_category->aggregation != GRADE_AGGREGATE_SUM) {
+                if ($mform->elementExists('weightoverride')) {
+                    $mform->removeElement('weightoverride');
+                }
+                if ($mform->elementExists('aggregationcoef2')) {
+                    $mform->removeElement('aggregationcoef2');
+                }
+            }
+
             if ($category = $grade_item->get_item_category()) {
                 if ($category->aggregation == GRADE_AGGREGATE_SUM) {
                     if ($mform->elementExists('gradetype')) {
index c483ff2..177fdb4 100644 (file)
@@ -42,10 +42,11 @@ class grade_edit_tree {
 
     public $deepest_level;
 
-    public $uses_extra_credit = false;
-
     public $uses_weight = false;
 
+    /** @var bool indicates if tree has categories with aggregation method other than Natural. */
+    protected $uses_non_natural = false;
+
     public $table;
 
     public $categories = array();
@@ -70,42 +71,32 @@ class grade_edit_tree {
         $this->gpr = $gpr;
         $this->deepest_level = $this->get_deepest_level($this->gtree->top_element);
 
-        $this->columns = array(grade_edit_tree_column::factory('name', array('deepest_level' => $this->deepest_level)),
-                               grade_edit_tree_column::factory('aggregation', array('flag' => true)));
+        $this->columns = array(grade_edit_tree_column::factory('name', array('deepest_level' => $this->deepest_level)));
 
-        if ($this->uses_weight) {
-            $this->columns[] = grade_edit_tree_column::factory('weight', array('adv' => 'aggregationcoef'));
+        if ($this->uses_non_natural) {
+            $this->columns[] = grade_edit_tree_column::factory('aggregation', array('flag' => true));
         }
-        if ($this->uses_extra_credit) {
-            $this->columns[] = grade_edit_tree_column::factory('extracredit', array('adv' => 'aggregationcoef'));
+
+        if ($this->uses_weight) {
+            $this->columns[] = grade_edit_tree_column::factory('weight', array('adv' => 'weight'));
         }
 
         $this->columns[] = grade_edit_tree_column::factory('range'); // This is not a setting... How do we deal with it?
-        $this->columns[] = grade_edit_tree_column::factory('aggregateonlygraded', array('flag' => true));
-        $this->columns[] = grade_edit_tree_column::factory('aggregatesubcats', array('flag' => true));
-        $this->columns[] = grade_edit_tree_column::factory('aggregateoutcomes', array('flag' => true));
-        $this->columns[] = grade_edit_tree_column::factory('droplow', array('flag' => true));
-        $this->columns[] = grade_edit_tree_column::factory('keephigh', array('flag' => true));
-        $this->columns[] = grade_edit_tree_column::factory('multfactor', array('adv' => true));
-        $this->columns[] = grade_edit_tree_column::factory('plusfactor', array('adv' => true));
         $this->columns[] = grade_edit_tree_column::factory('actions');
-        $this->columns[] = grade_edit_tree_column::factory('select');
 
-        $mode = ($USER->gradeediting[$COURSE->id]) ? 'advanced' : 'simple';
-
-        $widthstyle = '';
-        if ($mode == 'simple') {
-            $widthstyle = ' style="width:auto;" ';
+        if ($this->deepest_level > 1) {
+            $this->columns[] = grade_edit_tree_column::factory('select');
         }
 
         $this->table = new html_table();
         $this->table->id = "grade_edit_tree_table";
-        $this->table->cellpadding = 5;
-        $this->table->attributes['class'] = 'generaltable ' . $mode;
-        $this->table->style = $widthstyle;
+        $this->table->attributes['class'] = 'generaltable simple setup-grades';
+        if ($this->moving) {
+            $this->table->attributes['class'] .= ' moving';
+        }
 
         foreach ($this->columns as $column) {
-            if (!($this->moving && $column->hide_when_moving) && !$column->is_hidden($mode)) {
+            if (!($this->moving && $column->hide_when_moving)) {
                 $this->table->head[] = $column->get_header_cell();
             }
         }
@@ -143,6 +134,7 @@ class grade_edit_tree {
         }
 
         $actions = '';
+        $moveaction = '';
 
         if (!$is_category_item) {
             $actions .= $this->gtree->get_edit_icon($element, $this->gpr);
@@ -159,13 +151,12 @@ class grade_edit_tree {
             }
 
             $aurl = new moodle_url('index.php', array('id' => $COURSE->id, 'action' => 'moveselect', 'eid' => $eid, 'sesskey' => sesskey()));
-            $actions .= $OUTPUT->action_icon($aurl, new pix_icon('t/move', get_string('move')));
+            $moveaction .= $OUTPUT->action_icon($aurl, new pix_icon('t/move', get_string('move')));
         }
 
         $actions .= $this->gtree->get_hiding_icon($element, $this->gpr);
-        $actions .= $this->gtree->get_locking_icon($element, $this->gpr);
 
-        $mode = ($USER->gradeediting[$COURSE->id]) ? 'advanced' : 'simple';
+        $actions .= $this->gtree->get_reset_icon($element, $this->gpr);
 
         $returnrows = array();
         $root = false;
@@ -180,7 +171,8 @@ class grade_edit_tree {
             // do not diplay children
             $cell = new html_table_cell();
             $cell->colspan = 12;
-            $cell->attributes['class'] = $element['type'] . ' moving';
+            $cell->attributes['class'] = $element['type'] . ' moving column-name level' .
+                ($level + 1) . ' level' . ($level % 2 ? 'even' : 'odd');
             $cell->text = $object->name.' ('.get_string('move').')';
             return array(new html_table_row(array($cell)));
         }
@@ -225,7 +217,7 @@ class grade_edit_tree {
 
                     $strmove     = get_string('move');
                     $strmovehere = get_string('movehere');
-                    $actions = ''; // no action icons when moving
+                    $actions = $moveaction = ''; // no action icons when moving
 
                     $aurl = new moodle_url('index.php', array('id' => $COURSE->id, 'action' => 'move', 'eid' => $this->moving, 'moveafter' => $child_eid, 'sesskey' => sesskey()));
                     if ($first) {
@@ -234,6 +226,7 @@ class grade_edit_tree {
 
                     $cell = new html_table_cell();
                     $cell->colspan = 12;
+                    $cell->attributes['class'] = 'movehere level' . ($level + 1) . ' level' . ($level % 2 ? 'even' : 'odd');
 
                     $icon = new pix_icon('movehere', $strmovehere, null, array('class'=>'movetarget'));
                     $cell->text = $OUTPUT->action_icon($aurl, $icon);
@@ -285,7 +278,7 @@ class grade_edit_tree {
                 $root = true;
             }
 
-            $levelclass = "level$level";
+            $levelclass = "level$level level" . ($level % 2 ? 'odd' : 'even');
 
             $courseclass = '';
             if ($level == 1) {
@@ -302,13 +295,15 @@ class grade_edit_tree {
             $headercell->header = true;
             $headercell->scope = 'row';
             $headercell->attributes['title'] = $object->stripped_name;
-            $headercell->attributes['class'] = 'cell rowspan ' . $levelclass;
+            $headercell->attributes['class'] = 'cell column-rowspan rowspan ' . $levelclass;
             $headercell->rowspan = $row_count + 1;
             $row->cells[] = $headercell;
 
             foreach ($this->columns as $column) {
-                if (!($this->moving && $column->hide_when_moving) && !$column->is_hidden($mode)) {
-                    $row->cells[] = $column->get_category_cell($category, $levelclass, array('id' => $id, 'name' => $object->name, 'level' => $level, 'actions' => $actions, 'eid' => $eid));
+                if (!($this->moving && $column->hide_when_moving)) {
+                    $row->cells[] = $column->get_category_cell($category, $levelclass, array('id' => $id,
+                        'name' => $object->name, 'level' => $level, 'actions' => $actions,
+                        'moveaction' => $moveaction, 'eid' => $eid));
                 }
             }
 
@@ -319,7 +314,7 @@ class grade_edit_tree {
             // Print a coloured row to show the end of the category across the table
             $endcell = new html_table_cell();
             $endcell->colspan = (19 - $level);
-            $endcell->attributes['class'] = 'colspan ' . $levelclass;
+            $endcell->attributes['class'] = 'emptyrow colspan ' . $levelclass;
 
             $returnrows[] = new html_table_row(array($endcell));
 
@@ -333,6 +328,9 @@ class grade_edit_tree {
             if ($item->itemtype == 'category') {
                 $categoryitemclass = 'categoryitem';
             }
+            if ($item->itemtype == 'course') {
+                $categoryitemclass = 'courseitem';
+            }
 
             $dimmed = ($item->is_hidden()) ? "dimmed_text" : "";
             $gradeitemrow = new html_table_row();
@@ -342,9 +340,10 @@ class grade_edit_tree {
             }
 
             foreach ($this->columns as $column) {
-                if (!($this->moving && $column->hide_when_moving) && !$column->is_hidden($mode)) {
-                    $gradeitemrow->cells[] = $column->get_item_cell($item, array('id' => $id, 'name' => $object->name, 'level' => $level, 'actions' => $actions,
-                                                                 'element' => $element, 'eid' => $eid, 'itemtype' => $object->itemtype));
+                if (!($this->moving && $column->hide_when_moving)) {
+                    $gradeitemrow->cells[] = $column->get_item_cell($item, array('id' => $id, 'name' => $object->name,
+                        'level' => $level, 'actions' => $actions, 'element' => $element, 'eid' => $eid,
+                        'moveaction' => $moveaction, 'itemtype' => $object->itemtype));
                 }
             }
 
@@ -358,10 +357,9 @@ class grade_edit_tree {
     /**
      * Given a grade_item object, returns a labelled input if an aggregation coefficient (weight or extra credit) applies to it.
      * @param grade_item $item
-     * @param string type "extra" or "weight": the type of the column hosting the weight input
      * @return string HTML
      */
-    static function get_weight_input($item, $type) {
+    static function get_weight_input($item) {
         global $OUTPUT;
 
         if (!is_object($item) || get_class($item) !== 'grade_item') {
@@ -377,21 +375,60 @@ class grade_edit_tree {
         $parent_category->apply_forced_settings();
         $aggcoef = $item->get_coefstring();
 
-        if ((($aggcoef == 'aggregationcoefweight' || $aggcoef == 'aggregationcoef') && $type == 'weight') ||
-            ($aggcoef == 'aggregationcoefextraweight' && $type == 'extra')) {
-            return '<label class="accesshide" for="aggregationcoef_'.$item->id.'">'.
-                get_string('extracreditvalue', 'grades', $item->itemname).'</label>'.
-                '<input type="text" size="6" id="aggregationcoef_'.$item->id.'" name="aggregationcoef_'.$item->id.'"
+        $itemname = $item->itemname;
+        if ($item->is_category_item()) {
+            // Remember, the parent category of a category item is the category itself.
+            $itemname = $parent_category->get_name();
+        }
+        $str = '';
+
+        if ($aggcoef == 'aggregationcoefweight' || $aggcoef == 'aggregationcoef' || $aggcoef == 'aggregationcoefextraweight') {
+            return '<label class="accesshide" for="weight_'.$item->id.'">'.
+                get_string('extracreditvalue', 'grades', $itemname).'</label>'.
+                '<input type="text" size="6" id="weight_'.$item->id.'" name="weight_'.$item->id.'"
                 value="'.grade_edit_tree::format_number($item->aggregationcoef).'" />';
-        } elseif ($aggcoef == 'aggregationcoefextrasum' && $type == 'extra') {
-            $checked = ($item->aggregationcoef > 0) ? 'checked="checked"' : '';
-            return '<input type="hidden" name="extracredit_'.$item->id.'" value="0" />
-                <label class="accesshide" for="extracredit_'.$item->id.'">'.
-                get_string('extracreditvalue', 'grades', $item->itemname).'</label>
-                <input type="checkbox" id="extracredit_'.$item->id.'" name="extracredit_'.$item->id.'" value="1" '."$checked />\n";
-        } else {
-            return '';
+        } else if ($aggcoef == 'aggregationcoefextraweightsum') {
+
+            $checkboxname = 'weightoverride_' . $item->id;
+            $checkboxlbl = html_writer::tag('label', get_string('overrideweightofa', 'grades', $itemname),
+                array('for' => $checkboxname, 'class' => 'accesshide'));
+            $checkbox = html_writer::empty_tag('input', array('name' => $checkboxname,
+                'type' => 'hidden', 'value' => 0));
+            $checkbox .= html_writer::empty_tag('input', array('name' => $checkboxname,
+                'type' => 'checkbox', 'value' => 1, 'id' => $checkboxname, 'class' => 'weightoverride',
+                'checked' => ($item->weightoverride ? 'checked' : null)));
+
+            $name = 'weight_' . $item->id;
+            $hiddenlabel = html_writer::tag(
+                'label',
+                get_string('weightofa', 'grades', $itemname),
+                array(
+                    'class' => 'accesshide',
+                    'for' => $name
+                )
+            );
+
+            $input = html_writer::empty_tag(
+                'input',
+                array(
+                    'type' =>   'text',
+                    'size' =>   6,
+                    'id' =>     $name,
+                    'name' =>   $name,
+                    'value' =>  grade_edit_tree::format_number($item->aggregationcoef2 * 100.0),
+                    'disabled' => ($item->weightoverride ? null : 'disabled')
+                )
+            );
+
+            $str .= $checkboxlbl . $checkbox . $hiddenlabel . $input;
+
+            if ($item->aggregationcoef > 0) {
+                $str .= ' ' . html_writer::tag('abbr', get_string('aggregationcoefextrasumabbr', 'grades'),
+                        array('title' => get_string('aggregationcoefextrasum', 'grades')));
+            }
         }
+
+        return $str;
     }
 
     //Trims trailing zeros
@@ -448,7 +485,7 @@ class grade_edit_tree {
             $eids = array($eids);
         }
 
-        if(!$after_el = $this->gtree->locate_element("c$moveafter")) {
+        if(!$after_el = $this->gtree->locate_element("cg$moveafter")) {
             print_error('invalidelementid', '', $returnurl);
         }
 
@@ -486,10 +523,13 @@ class grade_edit_tree {
         $level++;
         $coefstring = $element['object']->get_coefstring();
         if ($element['type'] == 'category') {
-            if ($coefstring == 'aggregationcoefweight') {
+            if ($element['object']->aggregation != GRADE_AGGREGATE_SUM) {
+                $this->uses_non_natural = true;
+            }
+
+            if ($coefstring == 'aggregationcoefweight' || $coefstring == 'aggregationcoefextraweightsum' ||
+                    $coefstring == 'aggregationcoefextraweight') {
                 $this->uses_weight = true;
-            } elseif ($coefstring ==  'aggregationcoefextraweight' || $coefstring == 'aggregationcoefextrasum') {
-                $this->uses_extra_credit = true;
             }
 
             foreach($element['children'] as $child_el) {
@@ -504,6 +544,12 @@ class grade_edit_tree {
     }
 }
 
+/**
+ * Class grade_edit_tree_column
+ *
+ * @package   core_grades
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 abstract class grade_edit_tree_column {
     public $forced;
     public $hidden;
@@ -538,16 +584,27 @@ abstract class grade_edit_tree_column {
 
     public abstract function get_header_cell();
 
-    public abstract function get_category_cell($category, $levelclass, $params);
-
-    public abstract function get_item_cell($item, $params);
+    public function get_category_cell($category, $levelclass, $params) {
+        $cell = clone($this->categorycell);
+        $cell->attributes['class'] .= ' ' . $levelclass;
+        $cell->attributes['text'] = '';
+        return $cell;
+    }
 
-    public abstract function is_hidden($mode='simple');
+    public function get_item_cell($item, $params) {
+        $cell = clone($this->itemcell);
+        $cell->attributes['text'] = '';
+        if (isset($params['level'])) {
+            $level = $params['level'] + (($item->itemtype == 'category' || $item->itemtype == 'course') ? 0 : 1);
+            $cell->attributes['class'] .= ' level' . $level;
+            $cell->attributes['class'] .= ' level' . ($level % 2 ? 'odd' : 'even');
+        }
+        return $cell;
+    }
 
     public function __construct() {
         $this->headercell = new html_table_cell();
         $this->headercell->header = true;
-        $this->headercell->style = 'whitespace: normal;';
         $this->headercell->attributes['class'] = 'header';
 
         $this->categorycell = new html_table_cell();
@@ -555,35 +612,21 @@ abstract class grade_edit_tree_column {
 
         $this->itemcell = new html_table_cell();
         $this->itemcell->attributes['class'] = 'cell';
-    }
-}
-
-abstract class grade_edit_tree_column_category extends grade_edit_tree_column {
 
-    public $forced;
-    public $advanced;
-
-    public function __construct($name) {
-        global $CFG;
-        $this->forced = (int)$CFG->{"grade_$name"."_flag"} & 1;
-        $this->advanced = (int)$CFG->{"grade_$name"."_flag"} & 2;
-        parent::__construct();
-    }
-
-    public function is_hidden($mode='simple') {
-        global $CFG;
-        if ($mode == 'simple') {
-            return $this->advanced;
-        } elseif ($mode == 'advanced') {
-            if ($this->forced && $CFG->grade_hideforcedsettings) {
-                return true;
-            } else {
-                return false;
-            }
+        if (preg_match('/^grade_edit_tree_column_(\w*)$/', get_class($this), $matches)) {
+            $this->headercell->attributes['class'] .= ' column-' . $matches[1];
+            $this->categorycell->attributes['class'] .= ' column-' . $matches[1];
+            $this->itemcell->attributes['class'] .= ' column-' . $matches[1];
         }
     }
 }
 
+/**
+ * Class grade_edit_tree_column_name
+ *
+ * @package   core_grades
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 class grade_edit_tree_column_name extends grade_edit_tree_column {
     public $forced = false;
     public $hidden = false;
@@ -603,7 +646,6 @@ class grade_edit_tree_column_name extends grade_edit_tree_column {
 
     public function get_header_cell() {
         $headercell = clone($this->headercell);
-        $headercell->attributes['class'] .= ' name';
         $headercell->colspan = $this->deepest_level + 1;
         $headercell->text = get_string('name');
         return $headercell;
@@ -614,10 +656,10 @@ class grade_edit_tree_column_name extends grade_edit_tree_column {
         if (empty($params['name']) || empty($params['level'])) {
             throw new Exception('Array key (name or level) missing from 3rd param of grade_edit_tree_column_name::get_category_cell($category, $levelclass, $params)');
         }
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class'] .= ' name ' . $levelclass;
+        $moveaction = isset($params['moveaction']) ? $params['moveaction'] : '';
+        $categorycell = parent::get_category_cell($category, $levelclass, $params);
         $categorycell->colspan = ($this->deepest_level +1) - $params['level'];
-        $categorycell->text = $OUTPUT->heading($params['name'], 4);
+        $categorycell->text = $OUTPUT->heading($moveaction . $params['name'], 4);
         return $categorycell;
     }
 
@@ -629,20 +671,22 @@ class grade_edit_tree_column_name extends grade_edit_tree_column {
         }
 
         $name = $params['name'];
+        $moveaction = isset($params['moveaction']) ? $params['moveaction'] : '';
 
-        $itemcell = clone($this->itemcell);
-        $itemcell->attributes['class'] .= ' name';
+        $itemcell = parent::get_item_cell($item, $params);
         $itemcell->colspan = ($this->deepest_level + 1) - $params['level'];
-        $itemcell->text = $name;
+        $itemcell->text = $moveaction . $name;
         return $itemcell;
     }
-
-    public function is_hidden($mode='simple') {
-        return false;
-    }
 }
 
-class grade_edit_tree_column_aggregation extends grade_edit_tree_column_category {
+/**
+ * Class grade_edit_tree_column_aggregation
+ *
+ * @package   core_grades
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class grade_edit_tree_column_aggregation extends grade_edit_tree_column {
 
     public function __construct($params) {
         parent::__construct('aggregation');
@@ -661,106 +705,42 @@ class grade_edit_tree_column_aggregation extends grade_edit_tree_column_category
             throw new Exception('Array key (id) missing from 3rd param of grade_edit_tree_column_aggregation::get_category_cell($category, $levelclass, $params)');
         }
 
-        $options = array(GRADE_AGGREGATE_MEAN             => get_string('aggregatemean', 'grades'),
-                         GRADE_AGGREGATE_WEIGHTED_MEAN    => get_string('aggregateweightedmean', 'grades'),
-                         GRADE_AGGREGATE_WEIGHTED_MEAN2   => get_string('aggregateweightedmean2', 'grades'),
-                         GRADE_AGGREGATE_EXTRACREDIT_MEAN => get_string('aggregateextracreditmean', 'grades'),
-                         GRADE_AGGREGATE_MEDIAN           => get_string('aggregatemedian', 'grades'),
-                         GRADE_AGGREGATE_MIN              => get_string('aggregatemin', 'grades'),
-                         GRADE_AGGREGATE_MAX              => get_string('aggregatemax', 'grades'),
-                         GRADE_AGGREGATE_MODE             => get_string('aggregatemode', 'grades'),
-                         GRADE_AGGREGATE_SUM              => get_string('aggregatesum', 'grades'));
-
-        $visible = explode(',', $CFG->grade_aggregations_visible);
-        foreach ($options as $constant => $string) {
-            if (!in_array($constant, $visible) && $constant != $category->aggregation) {
-                unset($options[$constant]);
-            }
-        }
+        $options = grade_helper::get_aggregation_strings();
+        $aggregation = $options[$category->aggregation];
 
-        if ($this->forced) {
-            $aggregation = $options[$category->aggregation];
-        } else {
-            $attributes = array();
-            $attributes['id'] = 'aggregation_'.$category->id;
-            $attributes['class'] = 'ignoredirty';
-            $aggregation = html_writer::label(get_string('aggregation', 'grades'), 'aggregation_'.$category->id, false, array('class' => 'accesshide'));
-            $aggregation .= html_writer::select($options, 'aggregation_'.$category->id, $category->aggregation, null, $attributes);
-            $action = new component_action('change', 'update_category_aggregation', array('courseid' => $params['id'], 'category' => $category->id, 'sesskey' => sesskey()));
-            $OUTPUT->add_action_handler($action, 'aggregation_'.$category->id);
-        }
-
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class'] .= ' ' . $levelclass;
+        $categorycell = parent::get_category_cell($category, $levelclass, $params);
         $categorycell->text = $aggregation;
         return $categorycell;
 
     }
 
     public function get_item_cell($item, $params) {
-        $itemcell = clone($this->itemcell);
+        $itemcell = parent::get_item_cell($item, $params);
         $itemcell->text = ' - ';
         return $itemcell;
     }
 }
 
-class grade_edit_tree_column_extracredit extends grade_edit_tree_column {
-
-    public function get_header_cell() {
-        global $OUTPUT;
-        $headercell = clone($this->headercell);
-        $headercell->text = get_string('aggregationcoefextra', 'grades').$OUTPUT->help_icon('aggregationcoefextra', 'grades');
-        return $headercell;
-    }
-
-    public function get_category_cell($category, $levelclass, $params) {
-        $item = $category->get_grade_item();
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class'] .= ' ' . $levelclass;
-        $categorycell->text = grade_edit_tree::get_weight_input($item, 'extra');
-        return $categorycell;
-    }
-
-    public function get_item_cell($item, $params) {
-        if (empty($params['element'])) {
-            throw new Exception('Array key (element) missing from 2nd param of grade_edit_tree_column_weightorextracredit::get_item_cell($item, $params)');
-        }
-
-        $itemcell = clone($this->itemcell);
-        $itemcell->text = '&nbsp;';
-
-        if (!in_array($params['element']['object']->itemtype, array('courseitem', 'categoryitem', 'category'))) {
-            $itemcell->text = grade_edit_tree::get_weight_input($item, 'extra');
-        }
-
-        return $itemcell;
-    }
-
-    public function is_hidden($mode='simple') {
-        global $CFG;
-        if ($mode == 'simple') {
-            return strstr($CFG->grade_item_advanced, 'aggregationcoef');
-        } elseif ($mode == 'advanced') {
-            return false;
-        }
-    }
-}
-
+/**
+ * Class grade_edit_tree_column_weight
+ *
+ * @package   core_grades
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 class grade_edit_tree_column_weight extends grade_edit_tree_column {
 
     public function get_header_cell() {
         global $OUTPUT;
         $headercell = clone($this->headercell);
-        $headercell->text = get_string('weightuc', 'grades').$OUTPUT->help_icon('aggregationcoefweight', 'grades');
+        $headercell->text = get_string('weights', 'grades').$OUTPUT->help_icon('aggregationcoefweight', 'grades');
         return $headercell;
     }
 
     public function get_category_cell($category, $levelclass, $params) {
 
         $item = $category->get_grade_item();
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class']  .= ' ' . $levelclass;
-        $categorycell->text = grade_edit_tree::get_weight_input($item, 'weight');
+        $categorycell = parent::get_category_cell($category, $levelclass, $params);
+        $categorycell->text = grade_edit_tree::get_weight_input($item);
         return $categorycell;
     }
 
@@ -768,26 +748,23 @@ class grade_edit_tree_column_weight extends grade_edit_tree_column {
         if (empty($params['element'])) {
             throw new Exception('Array key (element) missing from 2nd param of grade_edit_tree_column_weightorextracredit::get_item_cell($item, $params)');
         }
-        $itemcell = clone($this->itemcell);
+        $itemcell = parent::get_item_cell($item, $params);
         $itemcell->text = '&nbsp;';
 
         if (!in_array($params['element']['object']->itemtype, array('courseitem', 'categoryitem', 'category'))) {
-            $itemcell->text = grade_edit_tree::get_weight_input($item, 'weight');
+            $itemcell->text = grade_edit_tree::get_weight_input($item);
         }
 
         return $itemcell;
     }
-
-    public function is_hidden($mode='simple') {
-        global $CFG;
-        if ($mode == 'simple') {
-            return strstr($CFG->grade_item_advanced, 'aggregationcoef');
-        } elseif ($mode == 'advanced') {
-            return false;
-        }
-    }
 }
 
+/**
+ * Class grade_edit_tree_column_range
+ *
+ * @package   core_grades
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 class grade_edit_tree_column_range extends grade_edit_tree_column {
 
     public function get_header_cell() {
@@ -797,8 +774,7 @@ class grade_edit_tree_column_range extends grade_edit_tree_column {
     }
 
     public function get_category_cell($category, $levelclass, $params) {
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class'] .= ' range ' . $levelclass;
+        $categorycell = parent::get_category_cell($category, $levelclass, $params);
         $categorycell->text = ' - ';
         return $categorycell;
     }
@@ -806,14 +782,12 @@ class grade_edit_tree_column_range extends grade_edit_tree_column {
     public function get_item_cell($item, $params) {
         global $DB, $OUTPUT;
 
-        // If the parent aggregation is Sum of Grades, we should show the number, even for scales, as that value is used...
+        // If the parent aggregation is Natural, we should show the number, even for scales, as that value is used...
         // ...in the computation. For text grades, the grademax is not used, so we can still show the no value string.
         $parent_cat = $item->get_parent_category();
         if ($item->gradetype == GRADE_TYPE_TEXT) {
             $grademax = ' - ';
-        } else if ($parent_cat->aggregation == GRADE_AGGREGATE_SUM) {
-            $grademax = format_float($item->grademax, $item->get_decimals());
-        } elseif ($item->gradetype == GRADE_TYPE_SCALE) {
+        } else if ($item->gradetype == GRADE_TYPE_SCALE) {
             $scale = $DB->get_record('scale', array('id' => $item->scaleid));
             $scale_items = null;
             if (empty($scale)) { //if the item is using a scale that's been removed
@@ -821,323 +795,28 @@ class grade_edit_tree_column_range extends grade_edit_tree_column {
             } else {
                 $scale_items = explode(',', $scale->scale);
             }
-            $grademax = end($scale_items) . ' (' . count($scale_items) . ')';
-        } elseif ($item->is_external_item()) {
-            $grademax = format_float($item->grademax, $item->get_decimals());
+            if ($parent_cat->aggregation == GRADE_AGGREGATE_SUM) {
+                $grademax = end($scale_items) . ' (' .
+                        format_float($item->grademax, $item->get_decimals()) . ')';
+            } else {
+                $grademax = end($scale_items) . ' (' . count($scale_items) . ')';
+            }
         } else {
-            $grademax = '<label class="accesshide" for="grademax'.$item->id.'">'.get_string('grademax', 'grades').'</label>
-                <input type="text" size="6" id="grademax'.$item->id.'" name="grademax_'.$item->id.'" value="'.
-                format_float($item->grademax, $item->get_decimals()).'" />';
+            $grademax = format_float($item->grademax, $item->get_decimals());
         }
 
-        $itemcell = clone($this->itemcell);
+        $itemcell = parent::get_item_cell($item, $params);
         $itemcell->text = $grademax;
         return $itemcell;
     }
-
-    public function is_hidden($mode='simple') {
-        global $CFG;
-        if ($mode == 'simple') {
-            return strstr($CFG->grade_item_advanced, 'grademax');
-        } elseif ($mode == 'advanced') {
-            return false;
-        }
-    }
-}
-
-class grade_edit_tree_column_aggregateonlygraded extends grade_edit_tree_column_category {
-
-    public function __construct($params) {
-        parent::__construct('aggregateonlygraded');
-    }
-
-    public function get_header_cell() {
-        global $OUTPUT;
-        $headercell = clone($this->headercell);
-        $headercell->style .= 'width: 40px;';
-        $headercell->text = get_string('aggregateonlygraded', 'grades')
-                . $OUTPUT->help_icon('aggregateonlygraded', 'grades');
-        return $headercell;
-    }
-
-    public function get_category_cell($category, $levelclass, $params) {
-        $onlygradedcheck = ($category->aggregateonlygraded == 1) ? 'checked="checked"' : '';
-        $hidden = '<input type="hidden" name="aggregateonlygraded_'.$category->id.'" value="0" />';
-        $aggregateonlygraded = '<label class="accesshide" for="aggregateonlygraded_'.$category->id.'">'.
-                get_string('aggregateonlygraded', 'grades').'</label>
-                <input type="checkbox" id="aggregateonlygraded_'.$category->id.'" name="aggregateonlygraded_'.
-                $category->id.'" value="1" '.$onlygradedcheck . ' />';
-
-        if ($this->forced) {
-            $aggregateonlygraded = ($category->aggregateonlygraded) ? get_string('yes') : get_string('no');
-        }
-
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class'] .= ' ' . $levelclass;
-        $categorycell->text = $hidden.$aggregateonlygraded;
-        return $categorycell;
-    }
-
-    public function get_item_cell($item, $params) {
-        $itemcell = clone($this->itemcell);
-        $itemcell->text = ' - ';
-        return $itemcell;
-    }
-}
-
-class grade_edit_tree_column_aggregatesubcats extends grade_edit_tree_column_category {
-
-    public function __construct($params) {
-        parent::__construct('aggregatesubcats');
-    }
-
-    public function get_header_cell() {
-        global $OUTPUT;
-        $headercell = clone($this->headercell);
-        $headercell->style .= 'width: 40px;';
-        $headercell->text = get_string('aggregatesubcats', 'grades')
-              .$OUTPUT->help_icon('aggregatesubcats', 'grades');
-        return $headercell;
-    }
-
-    public function get_category_cell($category, $levelclass, $params) {
-        $subcatscheck = ($category->aggregatesubcats == 1) ? 'checked="checked"' : '';
-        $hidden = '<input type="hidden" name="aggregatesubcats_'.$category->id.'" value="0" />';
-        $aggregatesubcats = '<label class="accesshide" for="aggregatesubcats_'.$category->id.'">'.
-                get_string('aggregatesubcats', 'grades').'</label>
-                <input type="checkbox" id="aggregatesubcats_'.$category->id.'" name="aggregatesubcats_'.$category->id.
-                '" value="1" ' . $subcatscheck.' />';
-
-        if ($this->forced) {
-            $aggregatesubcats = ($category->aggregatesubcats) ? get_string('yes') : get_string('no');
-        }
-
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class'] .= ' ' . $levelclass;
-        $categorycell->text = $hidden.$aggregatesubcats;
-        return $categorycell;
-
-    }
-
-    public function get_item_cell($item, $params) {
-        $itemcell = clone($this->itemcell);
-        $itemcell->text = ' - ';
-        return $itemcell;
-    }
-}
-
-class grade_edit_tree_column_aggregateoutcomes extends grade_edit_tree_column_category {
-
-    public function __construct($params) {
-        parent::__construct('aggregateoutcomes');
-    }
-
-    public function get_header_cell() {
-        global $OUTPUT;
-        $headercell = clone($this->headercell);
-        $headercell->style .= 'width: 40px;';
-        $headercell->text = get_string('aggregateoutcomes', 'grades')
-              .$OUTPUT->help_icon('aggregateoutcomes', 'grades');
-        return $headercell;
-    }
-
-    public function get_category_cell($category, $levelclass, $params) {
-        $outcomescheck = ($category->aggregateoutcomes == 1) ? 'checked="checked"' : '';
-        $hidden = '<input type="hidden" name="aggregateoutcomes_'.$category->id.'" value="0" />';
-        $aggregateoutcomes = '<label class="accesshide" for="aggregateoutcomes_'.$category->id.'">'.
-                get_string('aggregateoutcomes', 'grades').'</label>
-                <input type="checkbox" id="aggregateoutcomes_'.$category->id.'" name="aggregateoutcomes_'.$category->id.
-                '" value="1" ' . $outcomescheck.' />';
-
-        if ($this->forced) {
-            $aggregateoutcomes = ($category->aggregateoutcomes) ? get_string('yes') : get_string('no');
-        }
-
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class'] .= ' ' . $levelclass;
-        $categorycell->text = $hidden.$aggregateoutcomes;
-        return $categorycell;
-    }
-
-    public function get_item_cell($item, $params) {
-        $itemcell = clone($this->itemcell);
-        $itemcell->text = ' - ';
-        return $itemcell;
-    }
-
-    public function is_hidden($mode='simple') {
-        global $CFG;
-        if ($CFG->enableoutcomes) {
-            return parent::is_hidden($mode);
-        } else {
-            return true;
-        }
-    }
-}
-
-class grade_edit_tree_column_droplow extends grade_edit_tree_column_category {
-
-    public function __construct($params) {
-        parent::__construct('droplow');
-    }
-
-    public function get_header_cell() {
-        global $OUTPUT;
-        $headercell = clone($this->headercell);
-        $headercell->text = get_string('droplow', 'grades').$OUTPUT->help_icon('droplow', 'grades');
-        return $headercell;
-    }
-
-    public function get_category_cell($category, $levelclass, $params) {
-        $droplow = '<label class="accesshide" for="droplow_' . $category->id.'">' . get_string('droplowestvalue', 'grades') . '</label>';
-        $droplow .= '<input type="text" size="3" id="droplow_' . $category->id . '" name="droplow_' . $category->id . '" value="'
-                . $category->droplow.'" />';
-
-        if ($this->forced) {
-            $droplow = $category->droplow;
-        }
-
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class']  .= ' ' . $levelclass;
-        $categorycell->text = $droplow;
-        return $categorycell;
-    }
-
-    public function get_item_cell($item, $params) {
-        $itemcell = clone($this->itemcell);
-        $itemcell->text = ' - ';
-        return $itemcell;
-    }
-}
-
-class grade_edit_tree_column_keephigh extends grade_edit_tree_column_category {
-
-    public function __construct($params) {
-        parent::__construct('keephigh');
-    }
-
-    public function get_header_cell() {
-        global $OUTPUT;
-        $headercell = clone($this->headercell);
-        $headercell->text = get_string('keephigh', 'grades').$OUTPUT->help_icon('keephigh', 'grades');
-        return $headercell;
-    }
-
-    public function get_category_cell($category, $levelclass, $params) {
-        $keephigh = '<label class="accesshide" for="keephigh_'.$category->id.'">'.get_string('keephigh', 'grades').'</label>';
-        $keephigh .= '<input type="text" size="3" id="keephigh_'.$category->id.'" name="keephigh_'.$category->id.'" value="'.
-                $category->keephigh.'" />';
-
-        if ($this->forced) {
-            $keephigh = $category->keephigh;
-        }
-
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class'] .= ' ' . $levelclass;
-        $categorycell->text = $keephigh;
-        return $categorycell;
-    }
-
-    public function get_item_cell($item, $params) {
-        $itemcell = clone($this->itemcell);
-        $itemcell->text = ' - ';
-        return $itemcell;
-    }
-}
-
-class grade_edit_tree_column_multfactor extends grade_edit_tree_column {
-
-    public function __construct($params) {
-        parent::__construct();
-    }
-
-    public function get_header_cell() {
-        global $OUTPUT;
-        $headercell = clone($this->headercell);
-        $headercell->text = get_string('multfactor', 'grades').$OUTPUT->help_icon('multfactor', 'grades');
-        return $headercell;
-    }
-
-    public function get_category_cell($category, $levelclass, $params) {
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class'] .= ' ' . $levelclass;
-        $categorycell->text = ' - ';
-        return $categorycell;
-    }
-
-    public function get_item_cell($item, $params) {
-        global $OUTPUT;
-
-        $itemcell = clone($this->itemcell);
-        if (!$item->is_raw_used()) {
-            $itemcell->text = '&nbsp;';
-            return $itemcell;
-        }
-        $multfactor = '<label class="accesshide" for="multfactor'.$item->id.'">'.
-                get_string('multfactorvalue', 'grades', $item->itemname).'</label>
-                <input type="text" size="4" id="multfactor'.$item->id.'" name="multfactor_'.$item->id.'" value="'.
-                grade_edit_tree::format_number($item->multfactor).'" />';
-
-        $itemcell->text = $multfactor;
-        return $itemcell;
-    }
-
-    public function is_hidden($mode='simple') {
-        global $CFG;
-        if ($mode == 'simple') {
-            return strstr($CFG->grade_item_advanced, 'multfactor');
-        } elseif ($mode == 'advanced') {
-            return false;
-        }
-    }
-}
-
-class grade_edit_tree_column_plusfactor extends grade_edit_tree_column {
-
-    public function get_header_cell() {
-        global $OUTPUT;
-        $headercell = clone($this->headercell);
-        $headercell->text = get_string('plusfactor', 'grades').$OUTPUT->help_icon('plusfactor', 'grades');
-        return $headercell;
-    }
-
-    public function get_category_cell($category, $levelclass, $params) {
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class'] .= ' ' . $levelclass;
-        $categorycell->text = ' - ';
-        return $categorycell;
-
-    }
-
-    public function get_item_cell($item, $params) {
-        global $OUTPUT;
-
-        $itemcell = clone($this->itemcell);
-        if (!$item->is_raw_used()) {
-            $itemcell->text = '&nbsp;';
-            return $itemcell;
-        }
-
-        $plusfactor = '<label class="accesshide" for="plusfactor_'. $item->id . '">'.
-                get_string('plusfactorvalue', 'grades', $item->itemname).'</label>
-                <input type="text" size="4" id="plusfactor_'.$item->id.'" name="plusfactor_'.$item->id.'" value="'.
-                grade_edit_tree::format_number($item->plusfactor).'" />';
-
-        $itemcell->text = $plusfactor;
-        return $itemcell;
-
-    }
-
-    public function is_hidden($mode='simple') {
-        global $CFG;
-        if ($mode == 'simple') {
-            return strstr($CFG->grade_item_advanced, 'plusfactor');
-        } elseif ($mode == 'advanced') {
-            return false;
-        }
-    }
 }
 
+/**
+ * Class grade_edit_tree_column_actions
+ *
+ * @package   core_grades
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 class grade_edit_tree_column_actions extends grade_edit_tree_column {
 
     public function __construct($params) {
@@ -1146,7 +825,6 @@ class grade_edit_tree_column_actions extends grade_edit_tree_column {
 
     public function get_header_cell() {
         $headercell = clone($this->headercell);
-        $headercell->attributes['class'] .= ' actions';
         $headercell->text = get_string('actions');
         return $headercell;
     }
@@ -1157,8 +835,7 @@ class grade_edit_tree_column_actions extends grade_edit_tree_column {
             throw new Exception('Array key (actions) missing from 3rd param of grade_edit_tree_column_actions::get_category_actions($category, $levelclass, $params)');
         }
 
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class'] .= ' ' . $levelclass;
+        $categorycell = parent::get_category_cell($category, $levelclass, $params);
         $categorycell->text = $params['actions'];
         return $categorycell;
     }
@@ -1167,22 +844,22 @@ class grade_edit_tree_column_actions extends grade_edit_tree_column {
         if (empty($params['actions'])) {
             throw new Exception('Array key (actions) missing from 2nd param of grade_edit_tree_column_actions::get_item_cell($item, $params)');
         }
-        $itemcell = clone($this->itemcell);
-        $itemcell->attributes['class'] .= ' actions';
+        $itemcell = parent::get_item_cell($item, $params);
         $itemcell->text = $params['actions'];
         return $itemcell;
     }
-
-    public function is_hidden($mode='simple') {
-        return false;
-    }
 }
 
+/**
+ * Class grade_edit_tree_column_select
+ *
+ * @package   core_grades
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 class grade_edit_tree_column_select extends grade_edit_tree_column {
 
     public function get_header_cell() {
         $headercell = clone($this->headercell);
-        $headercell->attributes['class'] .= ' selection';
         $headercell->text = get_string('select');
         return $headercell;
     }
@@ -1195,9 +872,7 @@ class grade_edit_tree_column_select extends grade_edit_tree_column {
         $selectall  = new action_link(new moodle_url('#'), get_string('all'), new component_action('click', 'togglecheckboxes', array('eid' => $params['eid'], 'check' => true)));
         $selectnone = new action_link(new moodle_url('#'), get_string('none'), new component_action('click', 'togglecheckboxes', array('eid' => $params['eid'], 'check' => false)));
 
-        $categorycell = clone($this->categorycell);
-        $categorycell->attributes['class'] .= ' last ' . $levelclass;
-        $categorycell->style .= 'text-align: center;';
+        $categorycell = parent::get_category_cell($category, $levelclass, $params);
         $categorycell->text = $OUTPUT->render($selectall) . '<br />' . $OUTPUT->render($selectnone);
         return $categorycell;
     }
@@ -1206,20 +881,15 @@ class grade_edit_tree_column_select extends grade_edit_tree_column {
         if (empty($params['itemtype']) || empty($params['eid'])) {
             error('Array key (itemtype or eid) missing from 2nd param of grade_edit_tree_column_select::get_item_cell($item, $params)');
         }
-        $itemselect = '';
+        $itemcell = parent::get_item_cell($item, $params);
 
         if ($params['itemtype'] != 'course' && $params['itemtype'] != 'category') {
-            $itemselect = '<label class="accesshide" for="select_'.$params['eid'].'">'.
+            $itemcell->text = '<label class="accesshide" for="select_'.$params['eid'].'">'.
                 get_string('select', 'grades', $item->itemname).'</label>
                 <input class="itemselect ignoredirty" type="checkbox" name="select_'.$params['eid'].'" id="select_'.$params['eid'].
                 '" onchange="toggleCategorySelector();"/>'; // TODO: convert to YUI handler
         }
-        //html_writer::table() will wrap the item cell contents in a <TD> so don't do it here
-        return $itemselect;
-    }
-
-    public function is_hidden($mode='simple') {
-        return false;
+        return $itemcell;
     }
 }
 
index 460a379..6c5c330 100644 (file)
@@ -35,6 +35,9 @@ if ($id !== 0) {
     $url->param('id', $id);
 }
 $PAGE->set_url($url);
+$PAGE->set_pagelayout('admin');
+navigation_node::override_active_url(new moodle_url('/grade/edit/tree/index.php',
+    array('id'=>$courseid)));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
     print_error('nocourseid');
@@ -214,7 +217,8 @@ if ($data = $mform->get_data()) {
     redirect($returnurl);
 }
 
-print_grade_page_head($courseid, 'edittree', null, $heading);
+$PAGE->navbar->add($heading);
+print_grade_page_head($courseid, 'settings', null, $heading, false, false, false);
 
 if (!grade_outcome::fetch_all_available($COURSE->id)) {
     echo $OUTPUT->confirm(get_string('nooutcomes', 'grades'), $CFG->wwwroot.'/grade/edit/outcome/course.php?id='.$courseid, $returnurl);
index d3f13ac..643bdb9 100644 (file)
@@ -458,6 +458,48 @@ function grade_get_graded_users_select($report, $course, $userid, $groupid, $inc
     return $select;
 }
 
+/**
+ * Hide warning about changed grades during upgrade to 2.8.
+ *
+ * @param int $courseid The current course id.
+ */
+function hide_natural_aggregation_upgrade_notice($courseid) {
+    set_config('show_sumofgrades_upgrade_' . $courseid, false);
+}
+
+/**
+ * Print warning about changed grades during upgrade to 2.8.
+ *
+ * @param int $courseid The current course id.
+ * @param context $context The course context.
+ * @param boolean $return return as string
+ *
+ * @return nothing or string if $return true
+ */
+function print_natural_aggregation_upgrade_notice($courseid, $context, $return=false) {
+    global $OUTPUT;
+    $html = '';
+    $show = get_config('core', 'show_sumofgrades_upgrade_' . $courseid);
+
+    if ($show) {
+        $message = get_string('sumofgradesupgradedgrades', 'grades');
+        $hidemessage = get_string('sumofgradesupgradedgradeshidemessage', 'grades');
+        $urlparams = array( 'id' => $courseid,
+                            'seensumofgradesupgradedgrades' => true,
+                            'sesskey' => sesskey());
+        $goawayurl = new moodle_url('/grade/report/grader/index.php', $urlparams);
+        $goawaybutton = $OUTPUT->single_button($goawayurl, $hidemessage, 'get');
+        $html .= $OUTPUT->notification($message, 'notifysuccess');
+        $html .= $goawaybutton;
+    }
+
+    if ($return) {
+        return $html;
+    } else {
+        echo $html;
+    }
+}
+
 /**
  * Print grading plugin selection popup form.
  *
@@ -609,13 +651,6 @@ function grade_get_plugin_info($courseid, $active_type, $active_plugin) {
         $plugin_info['report'] = $reports;
     }
 
-    //showing grade categories and items make no sense if we're not within a course
-    if ($courseid!=$SITE->id) {
-        if ($edittree = grade_helper::get_info_edit_structure($courseid)) {
-            $plugin_info['edittree'] = $edittree;
-        }
-    }
-
     if ($scale = grade_helper::get_info_scales($courseid)) {
         $plugin_info['scale'] = array('view'=>$scale);
     }
@@ -650,11 +685,9 @@ function grade_get_plugin_info($courseid, $active_type, $active_plugin) {
         }
     }
 
-    //hide course settings if we're not in a course
-    if ($courseid!=$SITE->id) {
-        if ($setting = grade_helper::get_info_manage_settings($courseid)) {
-            $plugin_info['settings'] = array('course'=>$setting);
-        }
+    // Hide course settings if we're not in a course
+    if ($settings = grade_helper::get_info_manage_settings($courseid)) {
+        $plugin_info['settings'] = $settings;
     }
 
     // Put preferences last
@@ -778,7 +811,9 @@ function print_grade_page_head($courseid, $active_type, $active_plugin=null,
         $buttons = $OUTPUT->render($buttons);
     }
     $PAGE->set_button($buttons);
-    grade_extend_settings($plugin_info, $courseid);
+    if ($courseid != SITEID) {
+        grade_extend_settings($plugin_info, $courseid);
+    }
 
     $returnval = $OUTPUT->header();
     if (!$return) {
@@ -791,7 +826,8 @@ function print_grade_page_head($courseid, $active_type, $active_plugin=null,
     }
 
     if ($shownavigation) {
-        if ($CFG->grade_navmethod == GRADE_NAVMETHOD_COMBO || $CFG->grade_navmethod == GRADE_NAVMETHOD_DROPDOWN) {
+        if ($courseid != SITEID &&
+                ($CFG->grade_navmethod == GRADE_NAVMETHOD_COMBO || $CFG->grade_navmethod == GRADE_NAVMETHOD_DROPDOWN)) {
             $returnval .= print_grade_plugin_selector($plugin_info, $active_type, $active_plugin, $return);
         }
 
@@ -801,7 +837,8 @@ function print_grade_page_head($courseid, $active_type, $active_plugin=null,
             echo $OUTPUT->heading($heading);
         }
 
-        if ($CFG->grade_navmethod == GRADE_NAVMETHOD_COMBO || $CFG->grade_navmethod == GRADE_NAVMETHOD_TABS) {
+        if ($courseid != SITEID &&
+                ($CFG->grade_navmethod == GRADE_NAVMETHOD_COMBO || $CFG->grade_navmethod == GRADE_NAVMETHOD_TABS)) {
             $returnval .= grade_print_tabs($active_type, $active_plugin, $plugin_info, $return);
         }
     }
@@ -1171,19 +1208,22 @@ class grade_structure {
 
                 } else if (($is_course or $is_category) and ($is_scale or $is_value)) {
                     if ($category = $element['object']->get_item_category()) {
+                        $aggrstrings = grade_helper::get_aggregation_strings();
+                        $stragg = $aggrstrings[$category->aggregation];
                         switch ($category->aggregation) {
                             case GRADE_AGGREGATE_MEAN:
                             case GRADE_AGGREGATE_MEDIAN:
                             case GRADE_AGGREGATE_WEIGHTED_MEAN:
                             case GRADE_AGGREGATE_WEIGHTED_MEAN2:
                             case GRADE_AGGREGATE_EXTRACREDIT_MEAN:
-                                $stragg = get_string('aggregation', 'grades');
                                 return '<img src="'.$OUTPUT->pix_url('i/agg_mean') . '" ' .
                                         'class="icon itemicon" title="'.s($stragg).'" alt="'.s($stragg).'"/>';
                             case GRADE_AGGREGATE_SUM:
-                                $stragg = get_string('aggregation', 'grades');
                                 return '<img src="'.$OUTPUT->pix_url('i/agg_sum') . '" ' .
                                         'class="icon itemicon" title="'.s($stragg).'" alt="'.s($stragg).'"/>';
+                            default:
+                                return '<img src="'.$OUTPUT->pix_url('i/calc') . '" ' .
+                                        'class="icon itemicon" title="'.s($stragg).'" alt="'.s($stragg).'"/>';
                         }
                     }
 
@@ -1236,10 +1276,11 @@ class grade_structure {
      * @param bool  $withlink Whether or not this header has a link
      * @param bool  $icon Whether or not to display an icon with this header
      * @param bool  $spacerifnone return spacer if no icon found
+     * @param bool  $withdescription Show description if defined by this item.
      *
      * @return string header
      */
-    public function get_element_header(&$element, $withlink=false, $icon=true, $spacerifnone=false) {
+    public function get_element_header(&$element, $withlink=false, $icon=true, $spacerifnone=false, $withdescription=false) {
         $header = '';
 
         if ($icon) {
@@ -1264,6 +1305,13 @@ class grade_structure {
             }
         }
 
+        if ($withdescription) {
+            $desc = $element['object']->get_description();
+            if (!empty($desc)) {
+                $header .= '<div class="gradeitemdescription">' . s($desc) . '</div><div class="gradeitemdescriptionfiller"></div>';
+            }
+        }
+
         return $header;
     }
 
@@ -1416,7 +1464,7 @@ class grade_structure {
      * @return string eid
      */
     public function get_item_eid($grade_item) {
-        return 'i'.$grade_item->id;
+        return 'ig'.$grade_item->id;
     }
 
     /**
@@ -1454,6 +1502,34 @@ class grade_structure {
         return $strparams;
     }
 
+    /**
+     * Return a reset icon for the given element.
+     *
+     * @param array  $element An array representing an element in the grade_tree
+     * @param object $gpr A grade_plugin_return object
+     * @return string
+     */
+    public function get_reset_icon($element, $gpr) {
+        global $CFG, $OUTPUT;
+
+        // Limit to category items set to use the natural weights aggregation method, and users
+        // with the capability to manage grades.
+        if ($element['type'] != 'category' || $element['object']->aggregation != GRADE_AGGREGATE_SUM ||
+                !has_capability('moodle/grade:manage', $this->context)) {
+            return '';
+        }
+
+        $str = get_string('resetweights', 'grades', $this->get_params_for_iconstr($element));
+        $url = new moodle_url('/grade/edit/tree/action.php', array(
+            'id' => $this->courseid,
+            'action' => 'resetweights',
+            'eid' => $element['eid'],
+            'sesskey' => sesskey(),
+        ));
+
+        return $OUTPUT->action_icon($gpr->add_url_params($url), new pix_icon('t/reset', $str));
+    }
+
     /**
      * Return edit icon for give element
      *
@@ -1779,7 +1855,7 @@ class grade_seq extends grade_structure {
             $userid = $matches[2];
 
             //extra security check - the grade item must be in this tree
-            if (!$item_el = $this->locate_element('i'.$itemid)) {
+            if (!$item_el = $this->locate_element('ig'.$itemid)) {
                 return null;
             }
 
@@ -1795,7 +1871,7 @@ class grade_seq extends grade_structure {
                 return null;
             }
             //extra security check - the grade item must be in this tree
-            if (!$item_el = $this->locate_element('i'.$grade->itemid)) {
+            if (!$item_el = $this->locate_element('ig'.$grade->itemid)) {
                 return null;
             }
             $grade->grade_item =& $item_el['object']; // this may speedup grade_grade methods!
@@ -1993,9 +2069,9 @@ class grade_tree extends grade_structure {
 
         // prepare unique identifier
         if ($element['type'] == 'category') {
-            $element['eid'] = 'c'.$element['object']->id;
+            $element['eid'] = 'cg'.$element['object']->id;
         } else if (in_array($element['type'], array('item', 'courseitem', 'categoryitem'))) {
-            $element['eid'] = 'i'.$element['object']->id;
+            $element['eid'] = 'ig'.$element['object']->id;
             $this->items[$element['object']->id] =& $element['object'];
         }
 
@@ -2099,7 +2175,7 @@ class grade_tree extends grade_structure {
             $userid = $matches[2];
 
             //extra security check - the grade item must be in this tree
-            if (!$item_el = $this->locate_element('i'.$itemid)) {
+            if (!$item_el = $this->locate_element('ig'.$itemid)) {
                 return null;
             }
 
@@ -2115,7 +2191,7 @@ class grade_tree extends grade_structure {
                 return null;
             }
             //extra security check - the grade item must be in this tree
-            if (!$item_el = $this->locate_element('i'.$grade->itemid)) {
+            if (!$item_el = $this->locate_element('ig'.$grade->itemid)) {
                 return null;
             }
             $grade->grade_item =& $item_el['object']; // this may speedup grade_grade methods!
@@ -2343,8 +2419,11 @@ function grade_extend_settings($plugininfo, $courseid) {
         }
     }
 
-    if ($setting = grade_helper::get_info_manage_settings($courseid)) {
-        $gradenode->add(get_string('coursegradesettings', 'grades'), $setting->link, navigation_node::TYPE_SETTING, null, $setting->id, new pix_icon('i/settings', ''));
+    if ($settings = grade_helper::get_info_manage_settings($courseid)) {
+        $settingsnode = $gradenode->add($strings['settings'], null, navigation_node::TYPE_CONTAINER);
+        foreach ($settings as $setting) {
+            $settingsnode->add($setting->string, $setting->link, navigation_node::TYPE_SETTING, null, $setting->id, new pix_icon('i/settings', ''));
+        }
     }
 
     if ($preferences = grade_helper::get_plugins_report_preferences($courseid)) {
@@ -2368,13 +2447,6 @@ function grade_extend_settings($plugininfo, $courseid) {
         $gradenode->add($strings['scale'], $scales->link, navigation_node::TYPE_SETTING, null, $scales->id, new pix_icon('i/scales', ''));
     }
 
-    if ($categories = grade_helper::get_info_edit_structure($courseid)) {
-        $categoriesnode = $gradenode->add(get_string('categoriesanditems','grades'), null, navigation_node::TYPE_CONTAINER);
-        foreach ($categories as $category) {
-            $categoriesnode->add($category->string, $category->link, navigation_node::TYPE_SETTING, null, $category->id, new pix_icon('i/report', ''));
-        }
-    }
-
     if ($gradenode->contains_active_node()) {
         // If the gradenode is active include the settings base node (gradeadministration) in
         // the navbar, typcially this is ignored.
@@ -2424,11 +2496,6 @@ abstract class grade_helper {
      * @var grade_plugin_info|false
      */
     protected static $outcomeinfo = null;
-    /**
-     * Cached info on edit structure {@see get_info_edit_structure}
-     * @var array|false
-     */
-    protected static $edittree = null;
     /**
      * Cached leftter info {@see get_info_letters}
      * @var grade_plugin_info|false
@@ -2449,12 +2516,16 @@ abstract class grade_helper {
      * @var array
      */
     protected static $pluginstrings = null;
+    /**
+     * Cached grade aggregation strings
+     * @var array
+     */
+    protected static $aggregationstrings = null;
 
     /**
      * Gets strings commonly used by the describe plugins
      *
      * report => get_string('view'),
-     * edittree => get_string('edittree', 'grades'),
      * scale => get_string('scales'),
      * outcome => get_string('outcomes', 'grades'),
      * letter => get_string('letters', 'grades'),
@@ -2469,7 +2540,6 @@ abstract class grade_helper {
         if (self::$pluginstrings === null) {
             self::$pluginstrings = array(
                 'report' => get_string('view'),
-                'edittree' => get_string('edittree', 'grades'),
                 'scale' => get_string('scales'),
                 'outcome' => get_string('outcomes', 'grades'),
                 'letter' => get_string('letters', 'grades'),
@@ -2481,21 +2551,48 @@ abstract class grade_helper {
         }
         return self::$pluginstrings;
     }
+
+    /**
+     * Gets strings describing the available aggregation methods.
+     *
+     * @return array
+     */
+    public static function get_aggregation_strings() {
+        if (self::$aggregationstrings === null) {
+            self::$aggregationstrings = array(
+                GRADE_AGGREGATE_MEAN             => get_string('aggregatemean', 'grades'),
+                GRADE_AGGREGATE_WEIGHTED_MEAN    => get_string('aggregateweightedmean', 'grades'),
+                GRADE_AGGREGATE_WEIGHTED_MEAN2   => get_string('aggregateweightedmean2', 'grades'),
+                GRADE_AGGREGATE_EXTRACREDIT_MEAN => get_string('aggregateextracreditmean', 'grades'),
+                GRADE_AGGREGATE_MEDIAN           => get_string('aggregatemedian', 'grades'),
+                GRADE_AGGREGATE_MIN              => get_string('aggregatemin', 'grades'),
+                GRADE_AGGREGATE_MAX              => get_string('aggregatemax', 'grades'),
+                GRADE_AGGREGATE_MODE             => get_string('aggregatemode', 'grades'),
+                GRADE_AGGREGATE_SUM              => get_string('aggregatesum', 'grades')
+            );
+        }
+        return self::$aggregationstrings;
+    }
+
     /**
      * Get grade_plugin_info object for managing settings if the user can
      *
      * @param int $courseid
-     * @return grade_plugin_info
+     * @return grade_plugin_info[]
      */
     public static function get_info_manage_settings($courseid) {
         if (self::$managesetting !== null) {
             return self::$managesetting;
         }
         $context = context_course::instance($courseid);
-        if (has_capability('moodle/grade:manage', $context)) {
-            self::$managesetting = new grade_plugin_info('coursesettings', new moodle_url('/grade/edit/settings/index.php', array('id'=>$courseid)), get_string('course'));
-        } else {
-            self::$managesetting = false;
+        self::$managesetting = array();
+        if ($courseid != SITEID && has_capability('moodle/grade:manage', $context)) {
+            self::$managesetting['coursesettings'] = new grade_plugin_info('coursesettings',
+                new moodle_url('/grade/edit/settings/index.php', array('id'=>$courseid)),
+                get_string('coursegradesettings', 'grades'));
+            self::$managesetting['setup'] = new grade_plugin_info('setup',
+                new moodle_url('/grade/edit/tree/index.php', array('id' => $courseid)),
+                get_string('setupgradeslayout', 'grades'));
         }
         return self::$managesetting;
     }
@@ -2616,26 +2713,6 @@ abstract class grade_helper {
         }
         return self::$outcomeinfo;
     }
-    /**
-     * Get information on editing structures
-     * @param int $courseid
-     * @return array
-     */
-    public static function get_info_edit_structure($courseid) {
-        if (self::$edittree !== null) {
-            return self::$edittree;
-        }
-        if (has_capability('moodle/grade:manage', context_course::instance($courseid))) {
-            $url = new moodle_url('/grade/edit/tree/index.php', array('sesskey'=>sesskey(), 'showadvanced'=>'0', 'id'=>$courseid));
-            self::$edittree = array(
-                'simpleview' => new grade_plugin_info('simpleview', $url, get_string('simpleview', 'grades')),
-                'fullview' => new grade_plugin_info('fullview', new moodle_url($url, array('showadvanced'=>'1')), get_string('fullview', 'grades'))
-            );
-        } else {
-            self::$edittree = false;
-        }
-        return self::$edittree;
-    }
     /**
      * Get information on letters
      * @param int $courseid
@@ -2836,5 +2913,22 @@ abstract class grade_helper {
 
         return $fields;
     }
+
+    /**
+     * This helper method gets a snapshot of all the weights for a course.
+     * It is used as a quick method to see if any wieghts have been automatically adjusted.
+     * @param int $courseid
+     * @return array of itemid -> aggregationcoef2
+     */
+    public static function fetch_all_natural_weights_for_course($courseid) {
+        global $DB;
+        $result = array();
+
+        $records = $DB->get_records('grade_items', array('courseid'=>$courseid), 'id', 'id, aggregationcoef2');
+        foreach ($records as $record) {
+            $result[$record->id] = $record->aggregationcoef2;
+        }
+        return $result;
+    }
 }
 
index 66b9447..cc2f423 100644 (file)
@@ -128,6 +128,13 @@ $reportname = get_string('pluginname', 'gradereport_grader');
 // Print header
 print_grade_page_head($COURSE->id, 'report', 'grader', $reportname, false, $buttons);
 
+// Hide the following warning if the user told it to go away.
+if (optional_param('seensumofgradesupgradedgrades', false, PARAM_BOOL) && confirm_sesskey()) {
+    hide_natural_aggregation_upgrade_notice($courseid);
+}
+// This shows a notice about the upgrade to Natural aggregation.
+print_natural_aggregation_upgrade_notice($COURSE->id, $context);
+
 //Initialise the grader report object that produces the table
 //the class grade_report_grader_ajax was removed as part of MDL-21562
 $report = new grade_report_grader($courseid, $gpr, $context, $page, $sortitemid);
index cf02a6e..6aabf4c 100644 (file)
@@ -1049,6 +1049,12 @@ class grade_report_grader extends grade_report {
                     if ($item->needsupdate) {
                         $itemcell->text .= "<span class='gradingerror{$hidden}{$gradepass}'>" . $error . "</span>";
                     } else {
+                        // The max and min for an aggregation may be different to the grade_item.
+                        if (!is_null($gradeval)) {
+                            $item->grademax = $grade->rawgrademax;
+                            $item->grademin = $grade->rawgrademin;
+                        }
+
                         $itemcell->text .= "<span class='gradevalue{$hidden}{$gradepass}'>" .
                                 grade_format_gradevalue($gradeval, $item, true, $gradedisplaytype, null) . "</span>";
                         if ($showanalysisicon) {
@@ -1690,11 +1696,11 @@ class grade_report_grader extends grade_report {
     public static function do_process_action($target, $action, $courseid = null) {
         global $DB;
         // TODO: this code should be in some grade_tree static method
-        $targettype = substr($target, 0, 1);
-        $targetid = substr($target, 1);
+        $targettype = substr($target, 0, 2);
+        $targetid = substr($target, 2);
         // TODO: end
 
-        if ($targettype !== 'c') {
+        if ($targettype !== 'cg') {
             // The following code only works with categories.
             return true;
         }
index 31ae9a6..21f04b9 100644 (file)
@@ -414,7 +414,7 @@ abstract class grade_report {
      * @param string $courseid the course id
      * @param string $course_item an instance of grade_item
      * @param string $finalgrade the grade for the course_item
-     * @return array[] containing values for 'grade', 'grademax' and 'grademin'
+     * @return array[] containing values for 'grade', 'grademax', 'grademin', 'aggregationstatus' and 'aggregationweight'
      */
     protected function blank_hidden_total_and_adjust_bounds($courseid, $course_item, $finalgrade) {
         global $CFG, $DB;
@@ -426,8 +426,18 @@ abstract class grade_report {
         // If we're dealing with multiple courses we need to know when we've moved on to a new course.
         static $previous_courseid = null;
 
+        $coursegradegrade = grade_grade::fetch(array('userid'=>$this->user->id, 'itemid'=>$course_item->id));
         $grademin = $course_item->grademin;
         $grademax = $course_item->grademax;
+        if ($coursegradegrade) {
+            $grademin = $coursegradegrade->rawgrademin;
+            $grademax = $coursegradegrade->rawgrademax;
+        } else {
+            $coursegradegrade = new grade_grade(array('userid'=>$this->user->id, 'itemid'=>$course_item->id), false);
+        }
+        $hint = $coursegradegrade->get_aggregation_hint();
+        $aggregationstatus = $hint['status'];
+        $aggregationweight = $hint['weight'];
 
         if (!is_array($this->showtotalsifcontainhidden)) {
             debugging('showtotalsifcontainhidden should be an array', DEBUG_DEVELOPER);
@@ -435,7 +445,11 @@ abstract class grade_report {
         }
 
         if ($this->showtotalsifcontainhidden[$courseid] == GRADE_REPORT_SHOW_REAL_TOTAL_IF_CONTAINS_HIDDEN) {
-            return array('grade' => $finalgrade, 'grademin' => $grademin, 'grademax' => $grademax);
+            return array('grade' => $finalgrade,
+                         'grademin' => $grademin,
+                         'grademax' => $grademax,
+                         'aggregationstatus' => $aggregationstatus,
+                         'aggregationweight' => $aggregationweight);
         }
 
         // If we've moved on to another course or user, reload the grades.
@@ -471,19 +485,31 @@ abstract class grade_report {
         }
 
         //if the item definitely depends on a hidden item
-        if (array_key_exists($course_item->id, $hiding_affected['altered'])) {
+        if (array_key_exists($course_item->id, $hiding_affected['altered']) ||
+                array_key_exists($course_item->id, $hiding_affected['alteredgrademin']) ||
+                array_key_exists($course_item->id, $hiding_affected['alteredgrademax']) ||
+                array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus']) ||
+                array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) {
             if (!$this->showtotalsifcontainhidden[$courseid]) {
                 //hide the grade
                 $finalgrade = null;
             } else {
                 //use reprocessed marks that exclude hidden items
-                $finalgrade = $hiding_affected['altered'][$course_item->id];
-                if (!empty($hiding_affected['alteredgrademin'][$course_item->id])) {
+                if (array_key_exists($course_item->id, $hiding_affected['altered'])) {
+                    $finalgrade = $hiding_affected['altered'][$course_item->id];
+                }
+                if (array_key_exists($course_item->id, $hiding_affected['alteredgrademin'])) {
                     $grademin = $hiding_affected['alteredgrademin'][$course_item->id];
                 }
-                if (!empty($hiding_affected['alteredgrademax'][$course_item->id])) {
+                if (array_key_exists($course_item->id, $hiding_affected['alteredgrademax'])) {
                     $grademax = $hiding_affected['alteredgrademax'][$course_item->id];
                 }
+                if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus'])) {
+                    $aggregationstatus = $hiding_affected['alteredaggregationstatus'][$course_item->id];
+                }
+                if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) {
+                    $aggregationweight = $hiding_affected['alteredaggregationweight'][$course_item->id];
+                }
             }
         } else if (!empty($hiding_affected['unknown'][$course_item->id])) {
             //not sure whether or not this item depends on a hidden item
@@ -494,16 +520,22 @@ abstract class grade_report {
                 //use reprocessed marks that exclude hidden items
                 $finalgrade = $hiding_affected['unknown'][$course_item->id];
 
-                if (!empty($hiding_affected['alteredgrademin'][$course_item->id])) {
+                if (array_key_exists($course_item->id, $hiding_affected['alteredgrademin'])) {
                     $grademin = $hiding_affected['alteredgrademin'][$course_item->id];
                 }
-                if (!empty($hiding_affected['alteredgrademax'][$course_item->id])) {
+                if (array_key_exists($course_item->id, $hiding_affected['alteredgrademax'])) {
                     $grademax = $hiding_affected['alteredgrademax'][$course_item->id];
                 }
+                if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus'])) {
+                    $aggregationstatus = $hiding_affected['alteredaggregationstatus'][$course_item->id];
+                }
+                if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) {
+                    $aggregationweight = $hiding_affected['alteredaggregationweight'][$course_item->id];
+                }
             }
         }
 
-        return array('grade' => $finalgrade, 'grademin' => $grademin, 'grademax' => $grademax);
+        return array('grade' => $finalgrade, 'grademin' => $grademin, 'grademax' => $grademax, 'aggregationstatus'=>$aggregationstatus, 'aggregationweight'=>$aggregationweight);
     }
 
     /**
index 90ca93d..4bd2e61 100644 (file)
@@ -179,6 +179,13 @@ class grade_report_overview extends grade_report {
                         $course_item->grademax = $adjustedgrade['grademax'];
                         $course_item->grademin = $adjustedgrade['grademin'];
                     }
+                } else {
+                    // We must use the rawgrademin / rawgrademax because it can be different for
+                    // each grade_grade when items are excluded from sum of grades.
+                    if (!is_null($finalgrade)) {
+                        $course_item->grademin = $course_grade->rawgrademin;
+                        $course_item->grademax = $course_grade->rawgrademax;
+                    }
                 }
 
                 $data = array($courselink, grade_format_gradevalue($finalgrade, $course_item, true));
index 695ecf3..9c0660c 100644 (file)
@@ -129,6 +129,12 @@ class grade_report_user extends grade_report {
      */
     public $showlettergrade = false;
 
+    /**
+     * Show the calculated contribution to the course total column.
+     * @var bool
+     */
+    public $showcontributiontocoursetotal = false;
+
     /**
      * Show average grades in the report, default false.
      * @var false
@@ -170,6 +176,15 @@ class grade_report_user extends grade_report {
      */
     protected $viewasuser = false;
 
+    /**
+     * An array that collects the aggregationhints for every
+     * grade_item. The hints contain grade, grademin, grademax
+     * status, weight and parent.
+     *
+     * @var array
+     */
+    protected $aggregationhints = array();
+
     /**
      * Constructor. Sets local copies of user preferences and initialises grade_tree.
      * @param int $courseid
@@ -191,6 +206,7 @@ class grade_report_user extends grade_report {
         $this->showrange       = grade_get_setting($this->courseid, 'report_user_showrange',       !empty($CFG->grade_report_user_showrange));
         $this->showfeedback    = grade_get_setting($this->courseid, 'report_user_showfeedback',    !empty($CFG->grade_report_user_showfeedback));
         $this->showweight      = grade_get_setting($this->courseid, 'report_user_showweight',      !empty($CFG->grade_report_user_showweight));
+        $this->showcontributiontocoursetotal      = grade_get_setting($this->courseid, 'report_user_showcontributiontocoursetotal',      !empty($CFG->grade_report_user_showcontributiontocoursetotal));
         $this->showlettergrade = grade_get_setting($this->courseid, 'report_user_showlettergrade', !empty($CFG->grade_report_user_showlettergrade));
         $this->showaverage     = grade_get_setting($this->courseid, 'report_user_showaverage',     !empty($CFG->grade_report_user_showaverage));
 
@@ -327,6 +343,11 @@ class grade_report_user extends grade_report {
             $this->tablecolumns[] = 'feedback';
             $this->tableheaders[] = $this->get_lang_string('feedback', 'grades');
         }
+
+        if ($this->showcontributiontocoursetotal) {
+            $this->tablecolumns[] = 'contributiontocoursetotal';
+            $this->tableheaders[] = $this->get_lang_string('contributiontocoursetotal', 'grades');
+        }
     }
 
     function fill_table() {
@@ -338,6 +359,11 @@ class grade_report_user extends grade_report {
         return true;
     }
 
+    /**
+     * Fill the table with data.
+     *
+     * @param $element - An array containing the table data for the current row.
+     */
     private function fill_table_recursive(&$element) {
         global $DB, $CFG;
 
@@ -346,11 +372,12 @@ class grade_report_user extends grade_report {
         $grade_object = $element['object'];
         $eid = $grade_object->id;
         $element['userid'] = $this->user->id;
-        $fullname = $this->gtree->get_element_header($element, true, true, true);
+        $fullname = $this->gtree->get_element_header($element, true, true, true, true);
         $data = array();
         $hidden = '';
         $excluded = '';
-        $class = '';
+        $itemlevel = ($type == 'categoryitem' || $type == 'category' || $type == 'courseitem') ? $depth : ($depth + 1);
+        $class = 'level' . $itemlevel . ' level' . ($itemlevel % 2 ? 'odd' : 'even');
         $classfeedback = '';
 
         // If this is a hidden grade category, hide it completely from the user
@@ -408,13 +435,15 @@ class grade_report_user extends grade_report {
 
             if (!$hide) {
                 /// Excluded Item
+                /**
                 if ($grade_grade->is_excluded()) {
                     $fullname .= ' ['.get_string('excluded', 'grades').']';
                     $excluded = ' excluded';
                 }
+                **/
 
                 /// Other class information
-                $class = "$hidden $excluded";
+                $class .= $hidden . $excluded;
                 if ($this->switch) { // alter style based on whether aggregation is first or last
                    $class .= ($type == 'categoryitem' or $type == 'courseitem') ? " ".$alter."d$depth baggt b2b" : " item b1b";
                 } else {
@@ -433,17 +462,27 @@ class grade_report_user extends grade_report {
 
                 /// Actual Grade
                 $gradeval = $grade_grade->finalgrade;
+                $hint = $grade_grade->get_aggregation_hint();
                 if (!$this->canviewhidden) {
                     /// Virtual Grade (may be calculated excluding hidden items etc).
                     $adjustedgrade = $this->blank_hidden_total_and_adjust_bounds($this->courseid,
                                                                                  $grade_grade->grade_item,
                                                                                  $gradeval);
+
                     $gradeval = $adjustedgrade['grade'];
 
                     // We temporarily adjust the view of this grade item - because the min and
                     // max are affected by the hidden values in the aggregation.
                     $grade_grade->grade_item->grademax = $adjustedgrade['grademax'];
                     $grade_grade->grade_item->grademin = $adjustedgrade['grademin'];
+                    $hint['status'] = $adjustedgrade['aggregationstatus'];
+                    $hint['weight'] = $adjustedgrade['aggregationweight'];
+                } else {
+                    // The max and min for an aggregation may be different to the grade_item.
+                    if (!is_null($gradeval)) {
+                        $grade_grade->grade_item->grademax = $grade_grade->rawgrademax;
+                        $grade_grade->grade_item->grademin = $grade_grade->rawgrademin;
+                    }
                 }
 
                 if ($this->showfeedback) {
@@ -456,8 +495,13 @@ class grade_report_user extends grade_report {
                     $data['weight']['content'] = '-';
                     $data['weight']['headers'] = "$header_cat $header_row weight";
                     // has a weight assigned, might be extra credit
-                    if ($grade_object->aggregationcoef > 0 && $type <> 'courseitem') {
-                        $data['weight']['content'] = number_format($grade_object->aggregationcoef,2);
+
+                    // This obliterates the weight because it provides a more informative description.
+                    if (is_numeric($hint['weight'])) {
+                        $data['weight']['content'] = format_float($hint['weight'] * 100.0, 2) . ' %';
+                    }
+                    if ($hint['status'] != 'used' && $hint['status'] != 'unknown') {
+                        $data['weight']['content'] .= '<br>' . get_string('aggregationhint' . $hint['status'], 'grades');
                     }
                 }
 
@@ -586,6 +630,22 @@ class grade_report_user extends grade_report {
                     }
                     $data['feedback']['headers'] = "$header_cat $header_row feedback";
                 }
+                // Contribution to the course total column.
+                if ($this->showcontributiontocoursetotal) {
+                    $data['contributiontocoursetotal']['class'] = $class;
+                    $data['contributiontocoursetotal']['content'] = '-';
+                    $data['contributiontocoursetotal']['headers'] = "$header_cat $header_row contributiontocoursetotal";
+
+                    $hint['grademax'] = $grade_grade->grade_item->grademax;
+                    $hint['grademin'] = $grade_grade->grade_item->grademin;
+                    $hint['grade'] = $gradeval;
+                    $parent = $grade_object->load_parent_category();
+                    if ($grade_object->is_category_item()) {
+                        $parent = $parent->load_parent_category();
+                    }
+                    $hint['parent'] = $parent->load_grade_item()->id;
+                    $this->aggregationhints[$grade_grade->itemid] = $hint;
+                }
             }
         }
 
@@ -606,6 +666,9 @@ class grade_report_user extends grade_report {
         }
 
         /// Add this row to the overall system
+        foreach ($data as $key => $celldata) {
+            $data[$key]['class'] .= ' column-' . $key;
+        }
         $this->tabledata[] = $data;
 
         /// Recursively iterate through all child elements
@@ -614,6 +677,83 @@ class grade_report_user extends grade_report {
                 $this->fill_table_recursive($element['children'][$key]);
             }
         }
+
+        // Check we are showing this column, and we are looking at the root of the table.
+        // This should be the very last thing this fill_table_recursive function does.
+        if ($this->showcontributiontocoursetotal && ($type == 'category' && $depth == 1)) {
+            // We should have collected all the hints by now - walk the tree again and build the contributions column.
+
+            $this->fill_contributions_column($element);
+        }
+    }
+
+    /**
+     * This function is called after the table has been built and the aggregationhints
+     * have been collected. We need this info to walk up the list of parents of each
+     * grade_item.
+     *
+     * @param $element - An array containing the table data for the current row.
+     */
+    public function fill_contributions_column($element) {
+
+        // Recursively iterate through all child elements.
+        if (isset($element['children'])) {
+            foreach ($element['children'] as $key=>$child) {
+                $this->fill_contributions_column($element['children'][$key]);
+            }
+        } else if ($element['type'] == 'item') {
+            // This is a grade item (We don't do this for categories or we would double count).
+            $grade_object = $element['object'];
+            $itemid = $grade_object->id;
+
+            // Ignore anything with no hint - e.g. a hidden row.
+            if (isset($this->aggregationhints[$itemid])) {
+
+                // Normalise the gradeval.
+                $gradecat = $grade_object->load_parent_category();
+                if ($gradecat->aggregation == GRADE_AGGREGATE_SUM) {
+                    // Natural aggregation/Sum of grades does not consider the mingrade.
+                    $graderange = $this->aggregationhints[$itemid]['grademax'];
+                    $gradeval = $this->aggregationhints[$itemid]['grade'] / $graderange;
+                } else {
+                    $graderange = $this->aggregationhints[$itemid]['grademax'] - $this->aggregationhints[$itemid]['grademin'];
+                    $gradeval = ($this->aggregationhints[$itemid]['grade'] - $this->aggregationhints[$itemid]['grademin']) / $graderange;
+                }
+
+                // Multiply the normalised value by the weight
+                // of all the categories higher in the tree.
+                do {
+                    if (!is_null($this->aggregationhints[$itemid]['weight'])) {
+                        $gradeval *= $this->aggregationhints[$itemid]['weight'];
+                    }
+
+                    // The second part of this if is to prevent infinite loops
+                    // in case of crazy data.
+                    if (isset($this->aggregationhints[$itemid]['parent']) &&
+                            $this->aggregationhints[$itemid]['parent'] != $itemid) {
+                        $parent = $this->aggregationhints[$itemid]['parent'];
+                        $itemid = $parent;
+                    } else {
+                        // We are at the top of the tree.
+                        $parent = false;
+                    }
+                } while ($parent);
+                // Finally multiply by the course grademax.
+                $gradeval *= $this->aggregationhints[$itemid]['grademax'];
+
+                // Now we need to loop through the "built" table data and update the
+                // contributions column for the current row.
+                $header_row = "row_{$grade_object->id}_{$this->user->id}";
+                foreach ($this->tabledata as $key => $row) {
+                    if (isset($row['itemname']) && ($row['itemname']['id'] == $header_row)) {
+                        // Found it - update the column.
+                        $decimals = $grade_object->get_decimals();
+                        $this->tabledata[$key]['contributiontocoursetotal']['content'] = format_float($gradeval, $decimals, true);
+                        break;
+                    }
+                }
+            }
+        }
     }
 
     /**
@@ -632,10 +772,10 @@ class grade_report_user extends grade_report {
                    class='boxaligncenter generaltable user-grade'>
             <thead>
                 <tr>
-                    <th id='".$this->tablecolumns[0]."' class=\"header\" colspan='$maxspan'>".$this->tableheaders[0]."</th>\n";
+                    <th id='".$this->tablecolumns[0]."' class=\"header column-{$this->tablecolumns[0]}\" colspan='$maxspan'>".$this->tableheaders[0]."</th>\n";
 
         for ($i = 1; $i < count($this->tableheaders); $i++) {
-            $html .= "<th id='".$this->tablecolumns[$i]."' class=\"header\">".$this->tableheaders[$i]."</th>\n";
+            $html .= "<th id='".$this->tablecolumns[$i]."' class=\"header column-{$this->tablecolumns[$i]}\">".$this->tableheaders[$i]."</th>\n";
         }
 
         $html .= "
@@ -901,6 +1041,14 @@ function grade_report_user_settings_definition(&$mform) {
     }
 
     $mform->addElement('select', 'report_user_showlettergrade', get_string('showlettergrade', 'grades'), $options);
+    if (empty($CFG->grade_report_user_showcontributiontocoursetotal)) {
+        $options[-1] = get_string('defaultprev', 'grades', $options[0]);
+    } else {
+        $options[-1] = get_string('defaultprev', 'grades', $options[$CFG->grade_report_user_showcontributiontocoursetotal]);
+    }
+
+    $mform->addElement('select', 'report_user_showcontributiontocoursetotal', get_string('showcontributiontocoursetotal', 'grades'), $options);
+    $mform->addHelpButton('report_user_showcontributiontocoursetotal', 'showcontributiontocoursetotal', 'grades');
 
     if (empty($CFG->grade_report_user_showrange)) {
         $options[-1] = get_string('defaultprev', 'grades', $options[0]);
@@ -944,6 +1092,7 @@ function grade_report_user_settings_definition(&$mform) {
 
     $mform->addElement('select', 'report_user_showtotalsifcontainhidden', get_string('hidetotalifhiddenitems', 'grades'), $options);
     $mform->addHelpButton('report_user_showtotalsifcontainhidden', 'hidetotalifhiddenitems', 'grades');
+
 }
 
 /**
index d469f68..c85f4c8 100644 (file)
@@ -47,4 +47,5 @@ if ($ADMIN->fulltree) {
                                                       array(GRADE_REPORT_HIDE_TOTAL_IF_CONTAINS_HIDDEN => get_string('hide'),
                                                             GRADE_REPORT_SHOW_TOTAL_IF_CONTAINS_HIDDEN => get_string('hidetotalshowexhiddenitems', 'grades'),
                                                             GRADE_REPORT_SHOW_REAL_TOTAL_IF_CONTAINS_HIDDEN => get_string('hidetotalshowinchiddenitems', 'grades'))));
+    $settings->add(new admin_setting_configcheckbox('grade_report_user_showcontributiontocoursetotal', get_string('showcontributiontocoursetotal', 'grades'), get_string('showcontributiontocoursetotal_help', 'grades'), 0));
 }
index b481be4..e11851c 100644 (file)
@@ -57,3 +57,6 @@
 
 table.user-grade td.feedbacktext {text-align:left;width: 40%;font-size: 0.8em;white-space:normal;}
 table.user-grade td.itemcenter {text-align:center;}
+.user-grade .gradeitemdescription { font-weight: normal; position: absolute; }
+.user-grade .gradeitemdescriptionfiller { clear: left; height: 1em; }
+.user-grade .gradeitemdescription { font-weight: normal; position: absolute;}
index d410bd6..6272c83 100644 (file)
@@ -27,7 +27,8 @@
 
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given;
+use Behat\Behat\Context\Step\Given as Given,
+    Behat\Gherkin\Node\TableNode as TableNode;
 
 class behat_grade extends behat_base {
 
@@ -46,4 +47,26 @@ class behat_grade extends behat_base {
 
         return new Given('I set the field "' . $this->escape($fieldstr) . '" to "' . $grade . '"');
     }
+
+    /**
+     * Changes the settings of a grade item or category or the course.
+     *
+     * Teacher must be either on the grade setup page or on the Grader report page with editing mode turned on.
+     *
+     * @Given /^I set the following settings for grade item "(?P<grade_item_string>(?:[^"]|\\")*)":$/
+     * @param string $gradeitem
+     * @param TableNode $data
+     * @return Given[]
+     */
+    public function i_set_the_following_settings_for_grade_item($gradeitem, TableNode $data) {
+        $savechanges = get_string('savechanges', 'grades');
+        $edit = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('edit') . '  ');
+        $gradeitem = $this->getSession()->getSelectorsHandler()->xpathLiteral($gradeitem);
+        $linkxpath = "//a[./img[starts-with(@title,$edit) and contains(@title,$gradeitem)]]";
+        return array(
+            new Given('I click on "' . $this->escape($linkxpath) . '" "xpath_element"'),
+            new Given('I set the following fields to these values:', $data),
+            new Given('I press "' . $this->escape($savechanges) . '"'),
+        );
+    }
 }
index 42e6053..1df72b7 100644 (file)
@@ -18,8 +18,8 @@ Feature: We can use calculated grade totals
       | student1 | C1 | student |
     And the following "grade categories" exist:
       | fullname | course |
-      | Sub category 1 | C1|
-      | Sub category 2 | C1|
+      | Sub category 1 | C1 |
+      | Sub category 2 | C1 |
     And the following "activities" exist:
       | activity | course | idnumber | name | intro | grade |
       | assign | C1 | a1 | Test assignment one | Submit something! | 300 |
@@ -28,14 +28,18 @@ Feature: We can use calculated grade totals
       | 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 | 200
-      | assign | C1 | a6 | Test assignment six | Submit something! | Sub category 1 | 100
-      | assign | C1 | a7 | Test assignment seven | Submit nothing! | Sub category 1 | 150
+      | 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 the following "activities" exist:
       | activity | course | idnumber | name | intro | gradecategory | grade |
-      | assign | C1 | a8 | Test assignment eight | Submit something! | Sub category 2 | 200
-      | assign | C1 | a9 | Test assignment nine | Submit something! | Sub category 2 | 100
-      | assign | C1 | 10 | Test assignment ten | Submit nothing! | Sub category 2 | 150
+      | assign | C1 | a8 | Test assignment eight | Submit something! | Sub category 2 | 20 |
+      | assign | C1 | a9 | Test assignment nine | Submit something! | Sub category 2 | 10 |
+      | assign | C1 | 10 | Test assignment ten | Submit nothing! | Sub category 2 | 15 |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural |
+    And I log out
     And I log in as "teacher1"
     And I follow "Course 1"
     And I follow "Grades"
@@ -48,35 +52,28 @@ Feature: We can use calculated grade totals
     And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment eight"
     And I give the grade "5.00" to the user "Student 1" for the grade item "Test assignment nine"
     And I press "Save changes"
-    And I click on "Edit  assign Test assignment two" "link"
-    And I click on "Hidden" "checkbox"
-    And I press "Save changes"
-    And I click on "Edit  assign Test assignment five" "link"
-    And I click on "Hidden" "checkbox"
-    And I press "Save changes"
-    And I click on "Edit  assign Test assignment eight" "link"
-    And I click on "Hidden" "checkbox"
-    And I press "Save changes"
-    And I follow "Course grade settings"
+    And I set the following settings for grade item "Test assignment two":
+      | Hidden | 1 |
+    And I set the following settings for grade item "Test assignment five":
+      | Hidden | 1 |
+    And I set the following settings for grade item "Test assignment eight":
+      | Hidden | 1 |
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
     And I set the field "Grade display type" to "Real (percentage)"
     And I press "Save changes"
 
   @javascript
   Scenario: Mean of grades aggregation
-    And I follow "Edit   Course 1"
-    And I set the field "Aggregation" to "Mean of grades"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 1"
-    And I set the field "Aggregation" to "Mean of grades"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 2"
-    And I set the field "Aggregation" to "Mean of grades"
-    And I click on "Show more..." "link"
-    And I click on "Exclude empty grades" "checkbox"
-    And I press "Save changes"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation          | Mean of grades |
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation          | Mean of grades |
+    And I set the following settings for grade item "Sub category 2":
+      | Aggregation          | Mean of grades |
+      | Exclude empty grades | 0              |
     And I turn editing mode off
     Then I should see "30.00 (30.00 %)" in the ".course" "css_element"
-    And I follow "Course grade settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -88,27 +85,20 @@ Feature: We can use calculated grade totals
 
   @javascript
   Scenario: Weighted mean of grades aggregation
-    And I follow "Edit   Course 1"
-    And I set the field "Aggregation" to "Weighted mean of grades"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 1"
-    And I set the field "Aggregation" to "Weighted mean of grades"
-    And I expand all fieldsets
-    And I set the field "Item weight" to "1"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 2"
-    And I set the field "Aggregation" to "Weighted mean of grades"
-    And I expand all fieldsets
-    And I set the field "Item weight" to "1"
-    And I click on "Exclude empty grades" "checkbox"
-    And I press "Save changes"
-    And I click on "Edit  assign Test assignment one" "link"
-    And I expand all fieldsets
-    And I set the field "Item weight" to "3"
-    And I press "Save changes"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation          | Weighted mean of grades |
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation          | Weighted mean of grades |
+      | Item weight          | 1                       |
+    And I set the following settings for grade item "Sub category 2":
+      | Aggregation          | Weighted mean of grades |
+      | Item weight          | 1                       |
+      | Exclude empty grades | 0                       |
+    And I set the following settings for grade item "Test assignment one":
+      | Item weight | 3 |
     And I turn editing mode off
     Then I should see "27.14 (27.14 %)" in the ".course" "css_element"
-    And I follow "Course grade settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -120,24 +110,18 @@ Feature: We can use calculated grade totals
 
   @javascript
   Scenario: Simple weighted mean of grades aggregation
-    And I follow "Edit   Course 1"
-    And I set the field "Aggregation" to "Simple weighted mean of grades"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 1"
-    And I set the field "Aggregation" to "Simple weighted mean of grades"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 2"
-    And I set the field "Aggregation" to "Simple weighted mean of grades"
-    And I click on "Show more..." "link"
-    And I click on "Exclude empty grades" "checkbox"
-    And I press "Save changes"
-    And I click on "Edit  assign Test assignment one" "link"
-    And I expand all fieldsets
-    And I click on "Extra credit" "checkbox"
-    And I press "Save changes"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation          | Simple weighted mean of grades |
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation          | Simple weighted mean of grades |
+    And I set the following settings for grade item "Sub category 2":
+      | Aggregation          | Simple weighted mean of grades |
+      | Exclude empty grades | 0                              |
+    And I set the following settings for grade item "Test assignment one":
+      | Extra credit | 1 |
     And I turn editing mode off
     Then I should see "45.19 (45.19 %)" in the ".course" "css_element"
-    And I follow "Course grade settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -149,24 +133,18 @@ Feature: We can use calculated grade totals
 
   @javascript
   Scenario: Mean of grades (with extra credits) aggregation
-    And I follow "Edit   Course 1"
-    And I set the field "Aggregation" to "Mean of grades (with extra credits)"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 1"
-    And I set the field "Aggregation" to "Mean of grades (with extra credits)"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 2"
-    And I set the field "Aggregation" to "Mean of grades (with extra credits)"
-    And I click on "Show more..." "link"
-    And I click on "Exclude empty grades" "checkbox"
-    And I press "Save changes"
-    And I click on "Edit  assign Test assignment one" "link"
-    And I expand all fieldsets
-    And I set the field "Extra credit weight" to "2"
-    And I press "Save changes"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation          | Mean of grades (with extra credits) |
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation          | Mean of grades (with extra credits) |
+    And I set the following settings for grade item "Sub category 2":
+      | Aggregation          | Mean of grades (with extra credits) |
+      | Exclude empty grades | 0                                   |
+    And I set the following settings for grade item "Test assignment one":
+      | Extra credit weight  | 2 |
     And I turn editing mode off
     Then I should see "42.50 (42.50 %)" in the ".course" "css_element"
-    And I follow "Course grade settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -178,20 +156,16 @@ Feature: We can use calculated grade totals
 
   @javascript
   Scenario: Median of grades aggregation
-    And I follow "Edit   Course 1"
-    And I set the field "Aggregation" to "Median of grades"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 1"
-    And I set the field "Aggregation" to "Median of grades"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 2"
-    And I set the field "Aggregation" to "Median of grades"
-    And I click on "Show more..." "link"
-    And I click on "Exclude empty grades" "checkbox"
-    And I press "Save changes"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation | Median of grades |
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation | Median of grades |
+    And I set the following settings for grade item "Sub category 2":
+      | Aggregation          | Median of grades |
+      | Exclude empty grades | 0                |
     And I turn editing mode off
     Then I should see "26.67 (26.67 %)" in the ".course" "css_element"
-    And I follow "Course grade settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -203,26 +177,20 @@ Feature: We can use calculated grade totals
 
   @javascript
   Scenario: Lowest grade aggregation
-    And I follow "Edit   Course 1"
-    And I set the field "Aggregation" to "Lowest grade"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 1"
-    And I set the field "Aggregation" to "Lowest grade"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 2"
-    And I set the field "Aggregation" to "Lowest grade"
-    And I click on "Show more..." "link"
-    And I click on "Exclude empty grades" "checkbox"
-    And I press "Save changes"
-    And I click on "Edit  assign Test assignment five" "link"
-    And I click on "Hidden" "checkbox"
-    And I press "Save changes"
-    And I click on "Edit  assign Test assignment four" "link"
-    And I click on "Hidden" "checkbox"
-    And I press "Save changes"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation | Lowest grade |
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation | Lowest grade |
+    And I set the following settings for grade item "Sub category 2":
+      | Aggregation          | Lowest grade |
+      | Exclude empty grades | 0            |
+    And I set the following settings for grade item "Test assignment five":
+      | Hidden | 1 |
+    And I set the following settings for grade item "Test assignment four":
+      | Hidden | 1 |
     And I turn editing mode off
     Then I should see "0.00 (0.00 %)" in the ".course" "css_element"
-    And I follow "Course grade settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -234,23 +202,18 @@ Feature: We can use calculated grade totals
 
   @javascript
   Scenario: Highest grade aggregation
-    And I follow "Edit   Course 1"
-    And I set the field "Aggregation" to "Highest grade"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 1"
-    And I set the field "Aggregation" to "Highest grade"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 2"
-    And I set the field "Aggregation" to "Highest grade"
-    And I click on "Show more..." "link"
-    And I click on "Exclude empty grades" "checkbox"
-    And I press "Save changes"
-    And I click on "Edit  assign Test assignment one" "link"
-    And I click on "Hidden" "checkbox"
-    And I press "Save changes"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation          | Highest grade |
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation          | Highest grade |
+    And I set the following settings for grade item "Sub category 2":
+      | Aggregation          | Highest grade |
+      | Exclude empty grades | 0             |
+    And I set the following settings for grade item "Test assignment one":
+      | Hidden | 1 |
     And I turn editing mode off
     Then I should see "50.00 (50.00 %)" in the ".course" "css_element"
-    And I follow "Course grade settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -262,23 +225,18 @@ Feature: We can use calculated grade totals
 
   @javascript
   Scenario: Mode of grades aggregation
-    And I follow "Edit   Course 1"
-    And I set the field "Aggregation" to "Mode of grades"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 1"
-    And I set the field "Aggregation" to "Mode of grades"
-    And I press "Save changes"
-    And I follow "Edit   Sub category 2"
-    And I set the field "Aggregation" to "Mode of grades"
-    And I click on "Show more..." "link"
-    And I click on "Exclude empty grades" "checkbox"
-    And I press "Save changes"
-    And I click on "Edit  assign Test assignment one" "link"
-    And I click on "Hidden" "checkbox"
-    And I press "Save changes"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation          | Mode of grades |
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation          | Mode of grades |
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation          | Mode of grades |
+      | Exclude empty grades | 0              |
+    And I set the following settings for grade item "Test assignment one":
+      | Hidden | 1 |
     And I turn editing mode off
     Then I should see "50.00 (50.00 %)" in the ".course" "css_element"
-    And I follow "Course grade settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -289,24 +247,249 @@ Feature: We can use calculated grade totals
     And I should see "50.00 (50.00 %)" in the "overview-grade" "table"
 
   @javascript
-  Scenario: Sum of grades aggregation
-    And I follow "Edit   Sub category 1"
-    And I set the field "Aggregation" to "Sum of grades"
+  Scenario: Natural aggregation
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation          | Natural |
+      | Exclude empty grades | 0       |
+    And I set the following settings for grade item "Sub category 2":
+      | Aggregation          | Natural |
+      | Exclude empty grades | 1       |
+    And I set the following settings for grade item "Course 1":
+      | Aggregation          | Natural |
+      | Exclude empty grades | 0       |
+    And I set the following settings for grade item "Test assignment six":
+      | Weight adjusted  | 1   |
+      | aggregationcoef2 | 50  |
+    And I set the following settings for grade item "Test assignment three":
+      | Extra credit | 1 |
+    And I turn editing mode off
+    Then I should see "152.68 (24.43 %)" in the ".course" "css_element"
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I set the field "report_overview_showtotalsifcontainhidden" to "Show totals excluding hidden items"
+    And I set the field "report_user_showtotalsifcontainhidden" to "Show totals excluding hidden items"
+    And I set the field "Show contribution to course total" to "Show"
+    And I set the field "Show weightings" to "Show"
+    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"
+    And the following should exist in the "user-grade" table:
+      | Grade item | Calculated weight | Grade | Range | Contribution to course total |
+      | Test assignment five | 28.57 % | 10.00 (50.00 %) | 0–20 | 6.43 |
+      | Test assignment six | 50.00 % | 5.00 (50.00 %) | 0–10 | 11.25 |
+      | Test assignment seven | 21.43 % | - | 0–15 | 0.00 |
+      | Test assignment eight | 66.67 % | 10.00 (50.00 %) | 0–20 | 10.00 |
+      | Test assignment nine | 33.33 % | 5.00 (50.00 %) | 0–10 | 5.00 |
+      | Test assignment ten | -( Empty ) | - | 0–15 | 0.00 |
+      | Test assignment one | 48.00 % | 60.00 (20.00 %) | 0–300 | 60.00 |
+      | Test assignment two | 16.00 % | 20.00 (20.00 %) | 0–100 | 20.00 |
+      | Test assignment three | 24.00 %( Extra credit ) | 40.00 (26.67 %) | 0–150 | 40.00 |
+      | Test assignment four | 24.00 % | - | 0–150 | 0.00 |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I set the field "Grade report" to "Overview report"
+    And I should see "113.75 (23.45 %)" in the "overview-grade" "table"
+    And I set the field "Grade report" to "User report"
+    And the following should exist in the "user-grade" table:
+      | Grade item | Calculated weight | Grade | Range | Contribution to course total |
+      | Test assignment six | 70.00 % | 5.00 (50.00 %) | 0–10 | 8.75 |
+      | Test assignment seven | 30.00 % | - | 0–15 | 0.00 |
+      | Test assignment nine | 100.00 % | 5.00 (50.00 %) | 0–10 | 5.00 |
+      | Test assignment ten | -( Empty ) | - | 0–15 | 0.00 |
+      | Test assignment one | 61.86 % | 60.00 (20.00 %) | 0–300 | 60.0 |
+      | Test assignment three | 30.93 %( Extra credit ) | 40.00 (26.67 %) | 0–150 | 40.0 |
+      | Test assignment four | 30.93 % | - | 0–150 | 0.00 |
+
+  @javascript
+  Scenario: Natural aggregation with drop lowest
+    When I log out
+    And I log in as "admin"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I turn editing mode on
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation          | Natural |
+      | Exclude empty grades | 0       |
+    And I set the following settings for grade item "Sub category 2":
+      | Aggregation          | Natural |
+      | Exclude empty grades | 0       |
+    And I set the following settings for grade item "Course 1":
+      | Aggregation          | Natural |
+      | Exclude empty grades | 0       |
+    And I navigate to "Set up grades layout" node in "Grade administration > Settings"
+    And I press "Add category"
+    And I click on "Show more" "link"
+    And I set the following fields to these values:
+      | Category name | Sub category 3 |
+      | Aggregation | Natural |
+      | Drop the lowest | 1 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 1 |
+      | Grade category | Sub category 3 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 2 |
+      | Grade category | Sub category 3 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 3 |
+      | Grade category | Sub category 3 |
+    And I press "Save changes"
+    And I follow "Grader report"
+    And I give the grade "60.00" to the user "Student 1" for the grade item "Manual item 1"
+    And I give the grade "20.00" to the user "Student 1" for the grade item "Manual item 2"
+    And I give the grade "40.00" to the user "Student 1" for the grade item "Manual item 3"
     And I press "Save changes"
-    And I follow "Edit   Sub category 2"
-    And I set the field "Aggregation" to "Sum of grades"
+    And I turn editing mode off
+    Then I should see "250.00 (25.25 %)" in the ".course" "css_element"
+    And I turn editing mode on
+    And I set the following settings for grade item "Manual item 2":
+      | Extra credit | 1 |
+    And I turn editing mode off
+    And I should see "270.00 (27.27 %)" in the ".course" "css_element"
+    And I turn editing mode on
+    And I set the following settings for grade item "Manual item 2":
+      | Extra credit  | 0   |
+      | Maximum grade | 200 |
+    And I give the grade "21.00" to the user "Student 1" for the grade item "Manual item 2"
     And I press "Save changes"
-    And I follow "Edit   Course 1"
-    And I set the field "Aggregation" to "Sum of grades"
+    And I give the grade "20.00" to the user "Student 1" for the grade item "Manual item 2"
     And I press "Save changes"
     And I turn editing mode off
-    Then I should see "150.00 (18.99 %)" in the ".course" "css_element"
-    And I follow "Course grade settings"
-    And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
+    And I should see "270.00 (22.69 %)" in the ".course" "css_element"
+    And I turn editing mode on
+    And I set the following settings for grade item "Manual item 2":
+      | Extra credit  | 0   |
+      | Maximum grade | 100 |
+    And I give the grade "21.00" to the user "Student 1" for the grade item "Manual item 2"
+    And I press "Save changes"
+    And I give the grade "20.00" to the user "Student 1" for the grade item "Manual item 2"
+    And I press "Save changes"
+    And I turn editing mode off
+    And I should see "250.00 (25.25 %)" in the ".course" "css_element"
+    And I navigate to "Set up grades layout" node in "Grade administration > Settings"
+    And I press "Add category"
+    And I set the following fields to these values:
+      | Category name | Sub sub category 1 |
+      | Parent category | Sub category 3 |
+    And I press "Save changes"
+    And I follow "Grader report"
+    And I should see "270.00 (24.77 %)" in the ".course" "css_element"
+
+  @javascript
+  Scenario: Natural aggregation from the setup screen
+    And I set the field "Grade report" to "Set up grades layout"
+    And I follow "Edit   Course 1"
+    And I set the field "Aggregation" to "Natural"
+    And I press "Save changes"
+    And I follow "Edit   Sub category 1"
+    And I set the field "Aggregation" to "Natural"
     And I press "Save changes"
+    And I follow "Edit   Sub category 2"
+    And I set the field "Aggregation" to "Natural"
+    And I press "Save changes"
+
+    And I set the field "Override weight of Test assignment one" to "1"
+    And the field "Weight of Test assignment one" matches value "37.975"
+    And I set the field "Weight of Test assignment one" to "10"
+
+    And I set the field "Override weight of Test assignment two" to "1"
+    And the field "Weight of Test assignment two" matches value "12.658"
+    And I set the field "Override weight of Test assignment two" to "0"
+
+    And I set the field "Override weight of Test assignment six" to "1"
+    And the field "Weight of Test assignment six" matches value "22.222"
+    And I set the field "Weight of Test assignment six" to "50"
+    And I set the field "Override weight of Test assignment six" to "0"
+
+    And I set the field "Override weight of Test assignment ten" to "1"
+    And the field "Weight of Test assignment ten" matches value "33.333"
+    And I set the field "Weight of Test assignment ten" to "50"
+
+    And I set the field "Override weight of Sub category 1" to "1"
+    And the field "Weight of Sub category 1" matches value "5.696"
+    And I set the field "Weight of Sub category 1" to "15"
+
+    When I press "Save changes"
+    And I set the field "Override weight of Test assignment two" to "1"
+    And I set the field "Override weight of Test assignment six" to "1"
+
+    Then the field "Weight of Test assignment one" matches value "10.0"
+    And the field "Weight of Test assignment two" matches value "16.854"
+    And the field "Weight of Test assignment six" matches value "22.222"
+    And the field "Weight of Test assignment ten" matches value "50.0"
+    And the field "Weight of Sub category 1" matches value "15.0"
+    And I set the field "Override weight of Test assignment one" to "0"
+    And I set the field "Override weight of Test assignment two" to "0"
+    And I set the field "Override weight of Test assignment six" to "0"
+    And I set the field "Override weight of Sub category 1" to "0"
+    And I press "Save changes"
+    And I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Override weight of Sub category 1" to "1"
+    And the field "Weight of Test assignment one" matches value "37.975"
+    And the field "Weight of Sub category 1" matches value "5.696"
+    And I click on "Reset weights of Sub category 2" "link"
+    And the field "Weight of Test assignment ten" matches value "33.333"
+
+  @javascript
+  Scenario: Natural aggregation with weights of zero
+    When I set the following settings for grade item "Course 1":
+      | Aggregation          | Natural |
+      | Exclude empty grades | 0       |
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation          | Natural |
+      | Exclude empty grades | 0       |
+    And I set the following settings for grade item "Sub category 2":
+      | Aggregation          | Natural |
+      | Exclude empty grades | 0       |
+    And I turn editing mode off
+    And I set the field "Grade report" to "Set up grades layout"
+    And I set the field "Override weight of Test assignment one" to "1"
+    And I set the field "Weight of Test assignment one" to "0"
+    And I set the field "Override weight of Test assignment six" to "1"
+    And I set the field "Weight of Test assignment six" to "0"
+    And I set the field "Override weight of Test assignment nine" to "1"
+    And I set the field "Weight of Test assignment nine" to "100"
+    And I press "Save changes"
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I set the field "report_overview_showtotalsifcontainhidden" to "Show totals excluding hidden items"
+    And I set the field "report_user_showtotalsifcontainhidden" to "Show totals excluding hidden items"
+    And I set the field "Show contribution to course total" to "Show"
+    And I set the field "Show weightings" to "Show"
+    And I press "Save changes"
+    Then I should see "75.00 (16.85 %)" in the ".course" "css_element"
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    And the following should exist in the "user-grade" table:
+      | Grade item            | Calculated weight | Grade           | Contribution to course total |
+      | Test assignment five  | 57.14 %           | 10.00 (50.00 %) | 10.00                        |
+      | Test assignment six   | 0.00 %            | 5.00 (50.00 %)  | 0.00                         |
+      | Test assignment seven | 42.86 %           | -               | 0.00                         |
+      | Test assignment eight | 0.00 %            | 10.00 (50.00 %) | 0.00                         |
+      | Test assignment nine  | 100.00 %          | 5.00 (50.00 %)  | 5.00                         |
+      | Test assignment ten   | 0.00 %            | -               | 0.00                         |
+      | Test assignment one   | 0.00 %            | 60.00 (20.00 %) | 0.00                         |
+      | Test assignment two   | 22.47 %           | 20.00 (20.00 %) | 20.00                        |
+      | Test assignment three | 33.71 %           | 40.00 (26.67 %) | 40.00                        |
+      | Test assignment four  | 33.71 %           | -               | 0.00                         |
     And I log out
     And I log in as "student1"
     And I follow "Course 1"
     And I follow "Grades"
     And I set the field "Grade report" to "Overview report"
-    And I should see "110.00 (16.92 %)" in the "overview-grade" "table"
+    And I should see "45.00 (13.85 %)" in the "overview-grade" "table"
+    And I set the field "Grade report" to "User report"
+    And the following should exist in the "user-grade" table:
+      | Grade item            | Calculated weight | Grade           | Contribution to course total |
+      | Test assignment six   | 0.00 %            | 5.00 (50.00 %)  | 0.00                         |
+      | Test assignment seven | 100.00 %          | -               | 0.00                         |
+      | Test assignment nine  | 100.00 %          | 5.00 (50.00 %)  | 5.00                         |
+      | Test assignment ten   | 0.00              | -               | 0.00                         |
+      | Test assignment one   | 0.00 %            | 60.00 (20.00 %) | 0.00                         |
+      | Test assignment three | 46.15 %           | 40.00 (26.67 %) | 40.00                        |
+      | Test assignment four  | 46.15 %           | -               | 0.00                         |
diff --git a/grade/tests/behat/grade_calculated_weights.feature b/grade/tests/behat/grade_calculated_weights.feature
new file mode 100644 (file)
index 0000000..ccab3c0
--- /dev/null
@@ -0,0 +1,248 @@
+@core @core_grades
+Feature: We can understand the gradebook user report
+  In order to understand the gradebook user report
+  As an teacher
+  I need to see the calculated weights for each type of aggregation
+
+  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@asd.com | t1 |
+      | student1 | Student | 1 | student1@asd.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 |
+      | assign | C1 | a1 | Test assignment one | Submit something! |
+      | assign | C1 | a2 | Test assignment two | Submit something! |
+      | assign | C1 | a3 | Test assignment three | Submit something! |
+      | assign | C1 | a4 | Test assignment four | Submit something! |
+      | assign | C1 | a5 | Test assignment five | Submit something! |
+      | assign | C1 | a6 | Test assignment six | Submit something! |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I turn editing mode on
+    And I give the grade "60.00" to the user "Student 1" for the grade item "Test assignment one"
+    And I give the grade "20.00" to the user "Student 1" for the grade item "Test assignment two"
+    And I give the grade "40.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"
+    And I give the grade "70.00" to the user "Student 1" for the grade item "Test assignment five"
+    And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment six"
+    And I press "Save changes"
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I set the field "Show weightings" to "Show"
+    And I set the field "Show contribution to course total" to "Show"
+    And I press "Save changes"
+    And I set the field "Grade report" to "Set up grades layout"
+    And I press "Add category"
+    And I set the field "Category name" to "Sub category"
+    And I press "Save changes"
+    And I click on "Move" "link" in the "Test assignment six" "table_row"
+    # This xpath finds the forth last row in the table.
+    And I click on "Move to here" "link" in the "//tbody//tr[position()=last()-3]" "xpath_element"
+    And I click on "Move" "link" in the "Test assignment five" "table_row"
+    And I click on "Move to here" "link" in the "//tbody//tr[position()=last()-3]" "xpath_element"
+    And I click on "Move" "link" in the "Test assignment four" "table_row"
+    And I click on "Move to here" "link" in the "//tbody//tr[position()=last()-3]" "xpath_element"
+
+  @javascript
+  Scenario: Mean of grades aggregation
+    And I set the following settings for grade item "Course 1":
+      | Aggregation | Mean of grades |
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+
+    # Check the values in the weights column.
+    Then the following should exist in the "user-grade" table:
+      | Grade item | Calculated weight | Grade | Contribution to course total |
+      | Test assignment one | 25.00 % | 60.00 | 15.00 |
+      | Test assignment two | 25.00 % | 20.00 | 5.00 |
+      | Test assignment three | 25.00 % | 40.00 | 10.00 |
+      | Test assignment four | 33.33 % | 10.00 | 0.83 |
+      | Test assignment five | 33.33 % | 70.00 | 5.83 |
+      | Test assignment six | 33.33 % | 30.00 | 2.50 |
+
+  @javascript
+  Scenario: Weighted mean of grades aggregation
+    And I set the following settings for grade item "Course 1":
+      | Aggregation | Weighted mean of grades |
+    And I set the following settings for grade item "Test assignment one":
+      | Item weight | 2.0 |
+    And I set the following settings for grade item "Test assignment two":
+      | Item weight | 1.0 |
+    And I set the following settings for grade item "Test assignment three":
+      | Item weight | 1.0 |
+    And I set the following settings for grade item "Sub category":
+      | Item weight | 1.0 |
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+
+    # Check the values in the weights column.
+    Then the following should exist in the "user-grade" table:
+      | Grade item | Calculated weight | Grade | Contribution to course total |
+      | Test assignment one | 40.00 % | 60.00 | 24.00 |
+      | Test assignment two | 20.00 % | 20.00 | 4.00 |
+      | Test assignment three | 20.00 % | 40.00 | 8.00 |
+      | Test assignment four | 33.33 % | 10.00 | 0.67 |
+      | Test assignment five | 33.33 % | 70.00 | 4.67 |
+      | Test assignment six | 33.33 % | 30.00 | 2.00 |
+
+  @javascript
+  Scenario: Simple weighted mean of grades aggregation
+    And I set the following settings for grade item "Course 1":
+      | Aggregation | Simple weighted mean of grades |
+    And I set the following settings for grade item "Sub category":
+      | Aggregation | Simple weighted mean of grades |
+    And I set the following settings for grade item "Test assignment three":
+      | Extra credit | 1 |
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+
+    # Check the values in the weights column.
+    Then the following should exist in the "user-grade" table:
+      | Grade item | Calculated weight | Grade | Contribution to course total |
+      | Test assignment one | 33.33 % | 60.00 | 20.00 |
+      | Test assignment two | 33.33 % | 20.00 | 6.67 |
+      | Test assignment three | 33.33 %( Extra credit ) | 40.00 | 13.33 |
+      | Test assignment four | 33.33 % | 10.00 | 1.11 |
+      | Test assignment five | 33.33 % | 70.00 | 7.78 |
+      | Test assignment six | 33.33 % | 30.00 | 3.33 |
+
+  @javascript
+  Scenario: Mean of grades (with extra credits) aggregation
+    And I set the following settings for grade item "Course 1":
+      | Aggregation | Mean of grades (with extra credits) |
+    And I set the following settings for grade item "Test assignment three":
+      | Extra credit weight | 1.0 |
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+
+    # Check the values in the weights column.
+    Then the following should exist in the "user-grade" table:
+      | Grade item | Calculated weight | Grade | Contribution to course total |
+      | Test assignment one | 33.33 % | 60.00 | 20.00 |
+      | Test assignment two | 33.33 % | 20.00 | 6.67 |
+      | Test assignment three | 33.33 %( Extra credit ) | 40.00 | 13.33 |
+      | Test assignment four | 33.33 % | 10.00 | 1.11 |
+      | Test assignment five | 33.33 % | 70.00 | 7.78 |
+      | Test assignment six | 33.33 % | 30.00 | 3.33 |
+
+  @javascript
+  Scenario: Median of grades aggregation
+    And I set the following settings for grade item "Course 1":
+      | Aggregation | Median of grades |
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+
+    # Check the values in the weights column.
+    Then the following should exist in the "user-grade" table:
+      | Grade item | Calculated weight | Grade | Contribution to course total |
+      | Test assignment one | 0.00 % | 60.00 | 0.00 |
+      | Test assignment two | 0.00 % | 20.00 | 0.00 |
+      | Test assignment three | 50.00 % | 40.00 | 20.00 |
+      | Test assignment four | 33.33 % | 10.00 | 1.67 |
+      | Test assignment five | 33.33 % | 70.00 | 11.67 |
+      | Test assignment six | 33.33 % | 30.00 | 5.00 |
+
+  @javascript
+  Scenario: Lowest grade aggregation
+    And I set the following settings for grade item "Course 1":
+      | Aggregation | Lowest grade |
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+
+    # Check the values in the weights column.
+    Then the following should exist in the "user-grade" table:
+      | Grade item | Calculated weight | Grade | Contribution to course total |
+      | Test assignment one | 0.00 % | 60.00 | 0.00 |
+      | Test assignment two | 100.00 % | 20.00 | 20.00 |
+      | Test assignment three | 0.00 % | 40.00 | 0.00 |
+      | Test assignment four | 33.33 % | 10.00 | 0.00 |
+      | Test assignment five | 33.33 % | 70.00 | 0.00 |
+      | Test assignment six | 33.33 % | 30.00 | 0.00 |
+
+  @javascript
+  Scenario: Highest grade aggregation
+    And I set the following settings for grade item "Course 1":
+      | Aggregation | Highest grade |
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+
+    # Check the values in the weights column.
+    Then the following should exist in the "user-grade" table:
+      | Grade item | Calculated weight | Grade | Contribution to course total |
+      | Test assignment one | 100.00 % | 60.00 | 60.00 |
+      | Test assignment two | 0.00 % | 20.00 | 0.00 |
+      | Test assignment three | 0.00 % | 40.00 | 0.00 |
+      | Test assignment four | 33.33 % | 10.00 | 0.00 |
+      | Test assignment five | 33.33 % | 70.00 | 0.00 |
+      | Test assignment six | 33.33 % | 30.00 | 0.00 |
+
+  @javascript
+  Scenario: Mode of grades aggregation
+    And I set the following settings for grade item "Course 1":
+      | Aggregation | Mode of grades |
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+
+    # Check the values in the weights column.
+    Then the following should exist in the "user-grade" table:
+      | Grade item | Calculated weight | Grade | Contribution to course total |
+      | Test assignment one | 100.00 % | 60.00 | 60.00 |
+      | Test assignment two | 0.00 % | 20.00 | 0.00 |
+      | Test assignment three | 0.00 % | 40.00 | 0.00 |
+      | Test assignment four | 33.33 % | 10.00 | 0.00 |
+      | Test assignment five | 33.33 % | 70.00 | 0.00 |
+      | Test assignment six | 33.33 % | 30.00 | 0.00 |
+
+  @javascript
+  Scenario: View user report with mixed aggregation methods
+    And I set the following settings for grade item "Course 1":
+      | Aggregation | Natural |
+    And I set the following settings for grade item "Sub category":
+      | Aggregation | Weighted mean of grades |
+    And I set the following settings for grade item "Test assignment three":
+      | Extra credit | 1 |
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+
+    # Check the values in the weights column.
+    Then the following should exist in the "user-grade" table:
+      | Grade item | Calculated weight | Grade | Contribution to course total |
+      | Test assignment one | 33.33 % | 60.00 | 60.00 |
+      | Test assignment two | 33.33 % | 20.00 | 20.00 |
+      | Test assignment three | 33.33 %( Extra credit ) | 40.00 | 40.00 |
+      | Test assignment four | 33.33 % | 10.00 | 3.33 |
+      | Test assignment five | 33.33 % | 70.00 | 23.33 |
+      | Test assignment six | 33.33 % | 30.00 | 10.00 |
+      | Category totalWeighted mean of grades. | 33.33 % | 36.67 | - |
+      | Course totalNatural. | - | 156.67 | - |
+
+  @javascript
+  Scenario: View user report with natural aggregation
+    And I set the following settings for grade item "Test assignment three":
+      | Extra credit | 1 |
+    And I set the field "Grade report" to "User report"
+    And I set the field "Select all or one user" to "Student 1"
+
+    # Check the values in the weights column.
+    Then the following should exist in the "user-grade" table:
+      | Grade item | Calculated weight | Grade | Contribution to course total |
+      | Test assignment one | 20.00 % | 60.00 | 60.00 |
+      | Test assignment two | 20.00 % | 20.00 | 20.00 |
+      | Test assignment three | 20.00 %( Extra credit ) | 40.00 | 40.00 |
+      | Test assignment four | 33.33 % | 10.00 | 10.00 |
+      | Test assignment five | 33.33 % | 70.00 | 70.00 |
+      | Test assignment six | 33.33 % | 30.00 | 30.00 |
+      | Category totalNatural. | 60.00 % | 110.00 | - |
+      | Course totalNatural. | - | 230.00 | - |
diff --git a/grade/tests/behat/grade_mingrade.feature b/grade/tests/behat/grade_mingrade.feature
new file mode 100644 (file)
index 0000000..9be0f0b
--- /dev/null
@@ -0,0 +1,122 @@
+@core @core_grades
+Feature: We can use a minimum grade different than zero
+  In order to use a minimum grade different than zero
+  As an teacher
+  I need to set up a minimum grade different than zero
+
+  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@asd.com | t1 |
+      | student1 | Student | 1 | student1@asd.com | s1 |
+      | student2 | Student | 2 | student2@asd.com | s2 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+    And the following "grade categories" exist:
+      | fullname | course |
+      | Sub category 1 | C1 |
+      | Sub category 2 | C1 |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural |
+    And I am on homepage
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I navigate to "Set up grades layout" node in "Grade administration > Settings"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 1 |
+      | Minimum grade | -100 |
+      | Grade category | Course 1 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 2 |
+      | Minimum grade | 50 |
+      | Grade category | Course 1 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 3 |
+      | Maximum grade | 50 |
+      | Minimum grade | -100 |
+      | Grade category | Sub category 1 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 4 |
+      | Minimum grade | -100 |
+      | Grade category | Sub category 1 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 5 |
+      | Minimum grade | 50 |
+      | Grade category | Sub category 2 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Manual item 6 |
+      | Minimum grade | 50 |
+      | Grade category | Sub category 2 |
+    And I press "Save changes"
+    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I set the field "Show weightings" to "Show"
+    And I set the field "Show contribution to course total" to "Show"
+    And I press "Save changes"
+
+  @javascript
+  Scenario: Natural aggregation with negative and positive grade
+    And I navigate to "Set up grades layout" node in "Grade administration > Settings"
+    And I set the following settings for grade item "Sub category 1":
+      | Aggregation          | Natural |
+      | Exclude empty grades | 0       |
+    And I set the following settings for grade item "Sub category 2":
+      | Aggregation          | Natural |
+      | Exclude empty grades | 0       |
+    And I set the following settings for grade item "Course 1":
+      | Aggregation          | Natural |
+      | Exclude empty grades | 0       |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I turn editing mode on
+    When I give the grade "-25.00" to the user "Student 1" for the grade item "Manual item 1"
+    And I give the grade "50.00" to the user "Student 1" for the grade item "Manual item 2"
+    And I give the grade "-80.00" to the user "Student 1" for the grade item "Manual item 3"
+    And I give the grade "-10.00" to the user "Student 1" for the grade item "Manual item 4"
+    And I give the grade "50.00" to the user "Student 1" for the grade item "Manual item 5"
+    And I give the grade "75.00" to the user "Student 1" for the grade item "Manual item 6"
+    And I give the grade "0.00" to the user "Student 2" for the grade item "Manual item 1"
+    And I give the grade "0.00" to the user "Student 2" for the grade item "Manual item 2"
+    And I give the grade "-10.00" to the user "Student 2" for the grade item "Manual item 3"
+    And I give the grade "50.00" to the user "Student 2" for the grade item "Manual item 4"
+    And I give the grade "0.00" to the user "Student 2" for the grade item "Manual item 5"
+    And I give the grade "0.00" to the user "Student 2" for the grade item "Manual item 6"
+    And I press "Save changes"
+    And I follow "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  | Contribution to course total |
+      | Manual item 1 | 18.18 %           | -25.00 | -25.00                       |
+      | Manual item 2 | 18.18 %           | 50.00  | 50.00                        |
+      | Manual item 3 | 33.33 %           | -80.00 | -80.00                       |
+      | Manual item 4 | 66.67 %           | -10.00 | -10.00                       |
+      | Manual item 5 | 50.00 %           | 50.00  | 50.00                        |
+      | Manual item 6 | 50.00 %           | 75.00  | 75.00                        |
+    And I set the field "Select all or one user" to "Student 2"
+    And the following should exist in the "user-grade" table:
+      | Grade item    | Calculated weight | Grade  | Contribution to course total |
+      | Manual item 1 | 18.18 %           | 0.00   | 0.00                         |
+      | Manual item 2 | 18.18 %           | 50.00  | 50.00                        |
+      | Manual item 3 | 33.33 %           | -10.00 | -10.00                       |
+      | Manual item 4 | 66.67 %           | 50.00  | 50.00                        |
+      | Manual item 5 | 50.00 %           | 50.00  | 50.00                        |
+      | Manual item 6 | 50.00 %           | 50.00  | 50.00                        |
\ No newline at end of file
diff --git a/grade/tests/behat/grade_natural_normalisation.feature b/grade/tests/behat/grade_natural_normalisation.feature
new file mode 100644 (file)
index 0000000..d806c40
--- /dev/null
@@ -0,0 +1,252 @@
+@core @core_grades
+Feature: We can use natural aggregation and weights will be normalised to a total of one hundred
+  In order to override weights
+  As a teacher
+  I need to add assessments to the gradebook.
+
+  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@asd.com | t1 |
+      | student1 | Student | 1 | student1@asd.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 follow "Grades"
+    And I set the field "Grade report" to "Set up grades layout"
+
+  @javascript
+  Scenario: Setting all weights in a category to exactly one hundred in total.
+
+    And the field "Weight of Test assignment five" matches value "44.444"
+    And the field "Weight of Test assignment six" matches value "22.222"
+    And the field "Weight of Test assignment seven" matches value "33.333"
+    When 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 "Override weight of Test assignment seven" to "1"
+    And I set the field "Weight of Test assignment five" to "30"
+    And I set the field "Weight of Test assignment six" to "50"
+    And I set the field "Weight of Test assignment seven" to "20"
+    And I press "Save changes"
+
+    Then I should not see "Your weights have been adjusted to total 100."
+    And the field "Weight of Test assignment five" matches value "30.0"
+    And the field "Weight of Test assignment six" matches value "50.0"
+    And the field "Weight of Test assignment seven" matches value "20.0"
+
+  @javascript
+  Scenario: Setting all weights in a category to less than one hundred is normalised.
+
+    When 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 "Override weight of Test assignment seven" to "1"
+    And I set the field "Weight of Test assignment five" to "1"
+    And I set the field "Weight of Test assignment six" to "1"
+    And I set the field "Weight of Test assignment seven" to "2"
+    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 "25.0"
+    And the field "Weight of Test assignment six" matches value "25.0"
+    And the field "Weight of Test assignment seven" matches value "50.0"
+
+  @javascript
+  Scenario: Set one of the grade item weights to a figure over one hundred.
+
+    When I set the field "Override weight of Test assignment five" to "1"
+    And I set the field "Weight of Test assignment five" to "120"
+    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 "100.0"
+    And the field "Weight of Test assignment six" matches value "0.0"
+    And the field "Weight of Test assignment seven" matches value "0.0"
+
+  @javascript
+  Scenario: Setting several but not all grade item weights to over one hundred each.
+
+    When 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 "150"
+    And I set the field "Weight of Test assignment six" to "150"
+    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 "50.000"
+    And the field "Weight of Test assignment six" matches value "50.000"
+    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.
+
+    When I follow "Edit  assign Test assignment seven"
+    And I set the field "Extra credit" to "1"
+    And I press "Save changes"
+    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"
+    And I follow "Reset weights of 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.
+
+    When I follow "Edit  assign Test assignment seven"
+    And I set the field "Extra credit" to "1"
+    And I press "Save changes"
+    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"
+    And I follow "Reset weights of 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.
+
+    When I follow "Edit  assign Test assignment seven"
+    And I set the field "Extra credit" to "1"
+    And I press "Save changes"
+    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"
+    And I follow "Reset weights of 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.
+
+    When I follow "Edit  assign Test assignment seven"
+    And I set the field "Extra credit" to "1"
+    And I press "Save changes"
+    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"
+    And I follow "Reset weights of 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: The extra credit grade item weight is overridden to a figure over one hundred and then
+  the grade item is set to normal.
+
+    When I follow "Edit  assign Test assignment seven"
+    And I set the field "Extra credit" to "1"
+    And I press "Save changes"
+    And I set the field "Override weight of Test assignment seven" to "1"
+    And I set the field "Weight of Test assignment seven" to "105"
+    And I press "Save changes"
+    Then I should not see "Your weights have been adjusted to total 100."
+    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 "105.0"
+    And I follow "Edit  assign Test assignment seven"
+    And I set the field "Extra credit" to "0"
+    And I press "Save changes"
+    And I should see "Your weights have been adjusted to total 100."
+
+    And the field "Weight of Test assignment five" matches value "0.0"
+    And the field "Weight of Test assignment six" matches value "0.0"
+    And the field "Weight of Test assignment seven" matches value "100.0"
+
+  @javascript
+  Scenario: The extra credit grade item weight is overridden to a figure over one hundred and then
+  the grade category is reset.
+
+    When I follow "Edit  assign Test assignment seven"
+    And I set the field "Extra credit" to "1"
+    And I press "Save changes"
+    And I set the field "Override weight of Test assignment seven" to "1"
+    And I set the field "Weight of Test assignment seven" to "105"
+    And I press "Save changes"
+
+    And I follow "Reset weights of 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: Two out of three grade items weights are overridden and one is not.
+  The overridden grade item weights total over one hundred.
+
+    Given I set the field "Override weight of Test assignment six" to "1"
+    And I set the field "Override weight of Test assignment seven" to "1"
+    And I set the field "Weight of Test assignment six" to "55"
+    And I set the field "Weight of Test assignment seven" to "65"
+    And I press "Save changes"
+    And I should see "Your weights have been adjusted to total 100."
+
+    Then the field "Weight of Test assignment five" matches value "0.0"
+    And the field "Weight of Test assignment six" matches value "45.833"
+    And the field "Weight of Test assignment seven" matches value "54.167"
+
+  @javascript
+  Scenario: With one grade item set as extra credit, when I reset the weights for a category they return to the natural weights.
+
+  When I follow "Edit  assign Test assignment five"
+  And I set the field "Extra credit" to "1"
+  And I press "Save changes"
+  And I set the field "Override weight of Test assignment six" to "1"
+  And I set the field "Override weight of Test assignment seven" to "1"
+  And I set the field "Weight of Test assignment six" to "55"
+  And I set the field "Weight of Test assignment seven" to "40"
+  And I press "Save changes"
+  And I follow "Reset weights of Sub category 1"
+  Then the field "Weight of Test assignment five" matches value "80.0"
+  And the field "Weight of Test assignment six" matches value "40.0"
+  And the field "Weight of Test assignment seven" matches value "60.0"
diff --git a/grade/tests/behat/grade_scales.feature b/grade/tests/behat/grade_scales.feature
new file mode 100644 (file)
index 0000000..3c76991
--- /dev/null
@@ -0,0 +1,163 @@
+@core @core_grades
+Feature: View gradebook when scales are used
+  In order to use scales to grade activities
+  As an teacher
+  I need to be able to view gradebook with scales
+
+  Background:
+    Given I log in as "admin"
+    And I set the following administration settings values:
+      | grade_report_showranges    | 1 |
+      | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural |
+    And I navigate to "Scales" node in "Site administration > Grades"
+    And I press "Add a new scale"
+    And I set the following fields to these values:
+      | Name  | Letterscale |
+      | Scale | F,D,C,B,A   |
+    And I press "Save changes"
+    And I log out
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And the following "users" exist:
+      | username | firstname | lastname | email            | idnumber |
+      | teacher1 | Teacher   | 1        | teacher1@asd.com | t1       |
+      | student1 | Student   | 1        | student1@asd.com | s1       |
+      | student2 | Student   | 2        | student2@asd.com | s2       |
+      | student3 | Student   | 3        | student3@asd.com | s3       |
+      | student4 | Student   | 4        | student4@asd.com | s4       |
+      | student5 | Student   | 5        | student5@asd.com | s5       |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+      | student2 | C1     | student        |
+      | student3 | C1     | student        |
+      | student4 | C1     | student        |
+      | student5 | C1     | student        |
+    And the following "grade categories" exist:
+      | fullname       | course |
+      | Sub category 1 | C1     |
+    And the following "activities" exist:
+      | activity | course | idnumber | name                | intro             | gradecategory  |
+      | assign   | C1     | a1       | Test assignment one | Submit something! | Sub category 1 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Test assignment one"
+    And I follow "Edit settings"
+    And I expand all fieldsets
+    And I set the field "grade[modgrade_type]" to "Scale"
+    And I set the field "grade[modgrade_scale]" to "Letterscale"
+    And I press "Save and display"
+    And I follow "View/grade all submissions"
+    And I click on "Grade Student 1" "link" in the "Student 1" "table_row"
+    And I set the field "Grade" to "A"
+    And I press "Save and show next"
+    And I set the field "Grade" to "B"
+    And I press "Save and show next"
+    And I set the field "Grade" to "C"
+    And I press "Save and show next"
+    And I set the field "Grade" to "D"
+    And I press "Save and show next"
+    And I set the field "Grade" to "F"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I turn editing mode on
+
+  @javascript
+  Scenario: Test displaying scales in gradebook in aggregation method Natural
+    When I turn editing mode off
+    Then the following should exist in the "user-grades" table:
+      | -1-                | -4-      | -5-            | -6-          |
+      | Student 1          | A        | 5.00           | 5.00         |
+      | Student 2          | B        | 4.00           | 4.00         |
+      | Student 3          | C        | 3.00           | 3.00         |
+      | Student 4          | D        | 2.00           | 2.00         |
+      | Student 5          | F        | 1.00           | 1.00         |
+    And the following should exist in the "user-grades" table:
+      | -1-                | -2-      | -3-            | -4-          |
+      | Range              | F–A      | 0.00–5.00      | 0.00–5.00    |
+      | Overall average    | C        | 3.00           | 3.00         |
+    And I follow "User report"
+    And I set the field "Select all or one user" to "Student 3"
+    And I click on "Select all or one user" "select"
+    And the following should exist in the "user-grade" table:
+      | Grade item          | Grade | Range | Percentage |
+      | Test assignment one | C     | F–A   | 50.00 %    |
+      | Category totalNatural.      | 3.00  | 0–5   | 60.00 %    |
+      | Course totalNatural.        | 3.00  | 0–5   | 60.00 %    |
+    And I set the field "jump" to "Set up grades layout"
+    And the following should exist in the "grade_edit_tree_table" table:
+      | Name                | Max grade |
+      | Test assignment one | 5.00      |
+      | Category total      | 5.00      |
+      | Course total        | 5.00      |
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And the following should exist in the "user-grade" table:
+      | Grade item          | Grade | Range | Percentage |
+      | Test assignment one | B     | F–A   | 75.00 %    |
+      | Category totalNatural.      | 4.00  | 0–5   | 80.00 %    |
+      | Course totalNatural.        | 4.00  | 0–5   | 80.00 %    |
+
+  @javascript
+  Scenario Outline: Test displaying scales in gradebook in all other aggregation methods
+    When I follow "Edit   Course 1"
+    And I set the field "Aggregation" to "<aggregation>"
+    And I press "Save changes"
+    And I follow "Edit   Sub category 1"
+    And I expand all fieldsets
+    And I set the field "Aggregation" to "<aggregation>"
+    And I set the field "Category name" to "Sub category (<aggregation>)"
+    And I set the field "Maximum grade" to "5"
+    And I set the field "Minimum grade" to "1"
+    And I press "Save changes"
+    And I turn editing mode off
+    Then the following should exist in the "user-grades" table:
+      | -1-                | -4-      | -5-            | -6-            |
+      | Student 1          | A        | 5.00           | <coursetotal1> |
+      | Student 2          | B        | 4.00           | <coursetotal2> |
+      | Student 3          | C        | 3.00           | <coursetotal3> |
+      | Student 4          | D        | 2.00           | <coursetotal4> |
+      | Student 5          | F        | 1.00           | <coursetotal5> |
+    And the following should exist in the "user-grades" table:
+      | -1-                | -2-      | -3-            | -4-            |
+      | Range              | F–A      | 1.00–5.00      | 0.00–100.00    |
+      | Overall average    | C        | 3.00           | <overallavg>   |
+    And I follow "User report"
+    And I set the field "Select all or one user" to "Student 3"
+    And I click on "Sele