MDL-66722 forum: Add gradeitem storage handler
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 19 Sep 2019 00:01:18 +0000 (08:01 +0800)
committerMathew May <mathewm@hotmail.co.nz>
Wed, 30 Oct 2019 02:23:40 +0000 (10:23 +0800)
Part of MDL-66074

grade/classes/component_gradeitem.php [new file with mode: 0644]
grade/grading/form/lib.php
grade/tests/coverage.php
lang/en/grading.php
lib/gradelib.php
mod/forum/classes/grades/forum_gradeitem.php [new file with mode: 0644]
mod/forum/lib.php
mod/forum/tests/coverage.php
mod/forum/tests/grades_forum_gradeitem_test.php [new file with mode: 0644]
mod/forum/tests/grades_gradeitems_test.php

diff --git a/grade/classes/component_gradeitem.php b/grade/classes/component_gradeitem.php
new file mode 100644 (file)
index 0000000..c2d41bb
--- /dev/null
@@ -0,0 +1,447 @@
+<?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/>.
+
+/**
+ * Compontent definition of a gradeitem.
+ *
+ * @package   core_grades
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace core_grades;
+
+use context;
+use gradingform_controller;
+use gradingform_instance;
+use moodle_exception;
+use stdClass;
+use grade_item as core_gradeitem;
+use grading_manager;
+
+/**
+ * Compontent definition of a gradeitem.
+ *
+ * @package   core_grades
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class component_gradeitem {
+
+    /** @var array The scale data for the current grade item */
+    protected $scale;
+
+    /** @var string The component */
+    protected $component;
+
+    /** @var context The context for this activity */
+    protected $context;
+
+    /** @var string The item name */
+    protected $itemname;
+
+    /** @var int The grade itemnumber */
+    protected $itemnumber;
+
+    final protected function __construct(string $component, context $context, string $itemname) {
+        $this->component = $component;
+        $this->context = $context;
+        $this->itemname = $itemname;
+        $this->itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
+    }
+
+    /**
+     * Fetch an instance of a specific component_gradeitem.
+     *
+     * @param string $component
+     * @param context $context
+     * @param string $itemname
+     * @return self
+     */
+    public static function instance(string $component, context $context, string $itemname): self {
+        $itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
+
+        $classname = "{$component}\\grades\\{$itemname}_gradeitem";
+        if (!class_exists($classname)) {
+            throw new coding_exception("Unknown gradeitem {$itemname} for component {$classname}");
+        }
+
+        return $classname::load_from_context($context);
+    }
+
+    /**
+     * Load an instance of the current component_gradeitem based on context.
+     *
+     * @param context $context
+     * @return self
+     */
+    abstract public static function load_from_context(context $context): self;
+
+    /**
+     * The table name used for grading.
+     *
+     * @return string
+     */
+    abstract protected function get_table_name(): string;
+
+    /**
+     * Whether grading is enabled for this item.
+     *
+     * @return bool
+     */
+    abstract public function is_grading_enabled(): bool;
+
+    /**
+     * Get the grade value for this instance.
+     * The itemname is translated to the relevant grade field for the activity.
+     *
+     * @return int
+     */
+    abstract protected function get_gradeitem_value(): ?int;
+
+    /**
+     * Whether the grader can grade the gradee.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @return bool
+     */
+    abstract public function user_can_grade(stdClass $gradeduser, stdClass $grader): bool;
+
+    /**
+     * Require that the user can grade, throwing an exception if not.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @throws required_capability_exception
+     */
+    abstract public function require_user_can_grade(stdClass $gradeduser, stdClass $grader): void;
+
+    /**
+     * Get the scale if a scale is being used.
+     *
+     * @return stdClass
+     */
+    protected function get_scale(): ?stdClass {
+        $gradetype = $this->get_gradeitem_value();
+        if ($gradetype > 0) {
+            return null;
+        }
+
+        // This is a scale.
+        if (null === $this->scale) {
+            $this->scale = $DB->get_record('scale', ['id' => -1 * $gradetype]);
+        }
+
+        return $this->scale;
+    }
+
+    /**
+     * Check whether a scale is being used for this grade item.
+     *
+     * @return bool
+     */
+    protected function is_using_scale(): bool {
+        $gradetype = $this->get_gradeitem_value();
+
+        return $gradetype < 0;
+    }
+
+    /**
+     * Whether this grade item is configured to use direct grading.
+     *
+     * @return bool
+     */
+    public function is_using_direct_grading(): bool {
+        if ($this->is_using_scale()) {
+            return false;
+        }
+
+        if ($this->get_advanced_grading_controller()) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Whether this grade item is configured to use advanced grading.
+     *
+     * @return bool
+     */
+    public function is_using_advanced_grading(): bool {
+        if ($this->is_using_scale()) {
+            return false;
+        }
+
+        if ($this->get_advanced_grading_controller()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Get the name of the advanced grading method.
+     *
+     * @return string
+     */
+    public function get_advanced_grading_method(): ?string {
+        $gradingmanager = $this->get_grading_manager();
+
+        if (empty($gradingmanager)) {
+            return null;
+        }
+
+        return $gradingmanager->get_active_method();
+    }
+
+    /**
+     * Get the name of the component responsible for grading this gradeitem.
+     *
+     * @return string
+     */
+    public function get_grading_component_name(): ?string {
+        if (!$this->is_grading_enabled()) {
+            return null;
+        }
+
+        if ($method = $this->get_advanced_grading_method()) {
+            return "gradingform_{$method}";
+        }
+
+        return 'core_grades';
+    }
+
+    /**
+     * Get the name of the component subtype responsible for grading this gradeitem.
+     *
+     * @return string
+     */
+    public function get_grading_component_subtype(): ?string {
+        if (!$this->is_grading_enabled()) {
+            return null;
+        }
+
+        if ($method = $this->get_advanced_grading_method()) {
+            return null;
+        }
+
+        if ($this->is_using_scale()) {
+            return 'scale';
+        }
+
+        return 'point';
+    }
+
+    /**
+     * Whether decimals are allowed.
+     *
+     * @return bool
+     */
+    protected function allow_decimals(): bool {
+        return $this->get_gradeitem_value() > 0;
+    }
+
+    /**
+     * Get the grading manager for this advanced grading definition.
+     *
+     * @return grading_manager
+     */
+    protected function get_grading_manager(): ?grading_manager {
+        require_once(__DIR__ . '/../grading/lib.php');
+        return get_grading_manager($this->context, $this->component, $this->itemname);
+
+    }
+
+    /**
+     * Get the advanced grading controller if advanced grading is enabled.
+     *
+     * @return gradingform_controller
+     */
+    protected function get_advanced_grading_controller(): ?gradingform_controller {
+        $gradingmanager = $this->get_grading_manager();
+
+        if (empty($gradingmanager)) {
+            return null;
+        }
+
+        if ($gradingmethod = $gradingmanager->get_active_method()) {
+            return $gradingmanager->get_controller($gradingmethod);
+        }
+
+        return null;
+    }
+
+    /**
+     * Get the advanced grading menu items.
+     *
+     * @return array
+     */
+    protected function get_advanced_grading_grade_menu(): array {
+        return make_grades_menu($this->get_gradeitem_value());
+    }
+
+    /**
+     * Check whether the supplied grade is valid and throw an exception if not.
+     *
+     * @param float $grade The value being checked
+     * @throws moodle_exception
+     * @throws rating_exception
+     * @return bool
+     */
+    public function check_grade_validity(?float $grade): bool {
+        if ($this->is_using_scale()) {
+            // Fetch all options for this scale.
+            $scaleoptions = make_menu_from_list($this->get_scale());
+            if (!array_key_exists($grade, $scaleoptions)) {
+                // The selected option did not exist.
+                throw new rating_exception('ratinginvalid', 'rating');
+            }
+        } else if ($grade) {
+            $maxgrade = $this->get_gradeitem_value();
+            if ($grade > $maxgrade) {
+                // The grade is greater than the maximum possible value.
+                throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
+                    'maxgrade' => $maxgrade,
+                    'grade' => $grade,
+                ]);
+            } else if ($grade < 0) {
+                // Negative grades are not supported.
+                throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
+                    'maxgrade' => $maxgrade,
+                    'grade' => $grade,
+                ]);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Create an empty row in the grade for the specified user and grader.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @return stdClass The newly created grade record
+     */
+    abstract public function create_empty_grade(stdClass $gradeduser, stdClass $grader): stdClass;
+
+    /**
+     * Get the grade record for the specified grade id.
+     *
+     * @param int $gradeid
+     * @return stdClass
+     */
+    public function get_grade(int $gradeid): stdClass {
+        global $DB;
+
+        $grade = $DB->get_record($this->get_table_name(), ['id' => $gradeid]);
+
+        return $grade ?: null;
+    }
+
+    /**
+     * Get the grade for the specified user.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @return stdClass The grade value
+     */
+    abstract public function get_grade_for_user(stdClass $gradeduser, stdClass $grader): ?stdClass;
+
+    /**
+     * Get grades for all users for the specified gradeitem.
+     *
+     * @param int $itemnumber The specific grade item to fetch for the user
+     * @return stdClass[] The grades
+     */
+    abstract public function get_all_grades(): array;
+
+    /**
+     * Create or update the grade.
+     *
+     * @param stdClass $grade
+     * @return bool Success
+     */
+    abstract protected function store_grade(stdClass $grade): bool;
+
+    /**
+     * Create or update the grade.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @param stdClass $formdata The data submitted
+     * @return bool Success
+     */
+    public function store_grade_from_formdata(stdClass $gradeduser, stdClass $grader, stdClass $formdata): bool {
+        // Require gradelib for grade_floatval.
+        require_once(__DIR__ . '/../../lib/gradelib.php');
+        $grade = $this->get_grade_for_user($gradeduser, $grader);
+
+        if ($this->is_using_advanced_grading()) {
+            $instanceid = $formdata->instanceid;
+            $gradinginstance = $this->get_advanced_grading_instance($grader, $grade, (int) $instanceid);
+            $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading, $grade->id);
+        } else {
+            // Handle the case when grade is set to No Grade.
+            if (isset($formdata->grade)) {
+                $grade->grade = grade_floatval(unformat_float($formdata->grade));
+            }
+        }
+
+        return $this->store_grade($grade);
+    }
+
+    /**
+     * Get the advanced grading instance for the specified grade entry.
+     *
+     * @param stdClass $grader The user who is grading
+     * @param stdClass $grade The row from the grade table.
+     * @param int $instanceid The instanceid of the advanced grading form
+     * @return gradingform_instance
+     */
+    public function get_advanced_grading_instance(stdClass $grader, stdClass $grade, int $instanceid = null): ?gradingform_instance {
+        $controller = $this->get_advanced_grading_controller($this->itemname);
+
+        if (empty($controller)) {
+            // Advanced grading not enabeld for this item.
+            return null;
+        }
+
+        if (!$controller->is_form_available()) {
+            // The form is not available for this item.
+            return null;
+        }
+
+        // Fetch the instance for the specified graderid/itemid.
+        $gradinginstance = $controller->fetch_instance(
+            (int) $grader->id,
+            (int) $grade->id,
+            $instanceid
+        );
+
+        // Set the allowed grade range.
+        $gradinginstance->get_controller()->set_grade_range(
+            $this->get_advanced_grading_grade_menu(),
+            $this->allow_decimals()
+        );
+
+        return $gradinginstance;
+    }
+}
index 2f06662..a0653e8 100644 (file)
@@ -523,11 +523,49 @@ abstract class gradingform_controller {
      * @return gradingform_instance
      */
     public function get_or_create_instance($instanceid, $raterid, $itemid) {
+        if (!is_numeric($instanceid)) {
+            $instanceid = null;
+        }
+        return $this->fetch_instance($raterid, $itemid, $instanceid);
+    }
+
+    /**
+     * If an instanceid is specified and grading instance exists and it is created by this rater for
+     * this item, then the instance is returned.
+     *
+     * If instanceid is not known, then null can be passed to fetch the current instance matchign the specified raterid
+     * and itemid.
+     *
+     * If the instanceid is falsey, or no instance was found, then create a new instance for the specified rater and item.
+     *
+     * @param int $instanceid
+     * @param int $raterid
+     * @param int $itemid
+     * @return gradingform_instance
+     */
+    public function fetch_instance(int $raterid, int $itemid, ?int $instanceid): gradingform_instance {
         global $DB;
-        if ($instanceid &&
-                $instance = $DB->get_record('grading_instances', array('id'  => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) {
-            return $this->get_instance($instance);
+
+        $instance = null;
+        if (null === $instanceid) {
+            if ($instance = $this->get_current_instance($raterid, $itemid)) {
+                return $instance;
+            }
+            $instanceid = $instancerecord->id ?? null;
+        }
+
+        if (!empty($instanceid)) {
+            $instance = $DB->get_record('grading_instances', [
+                'id'  => $instanceid,
+                'raterid' => $raterid,
+                'itemid' => $itemid,
+            ], '*', IGNORE_MISSING);
+
+            if ($instance) {
+                return $this->get_instance($instance);
+            }
         }
+
         return $this->create_instance($raterid, $itemid);
     }
 
index 959e9c2..187e453 100644 (file)
@@ -35,6 +35,6 @@ defined('MOODLE_INTERNAL') || die();
 return new class extends phpunit_coverage_info {
     /** @var array The list of folders relative to the plugin root to whitelist in coverage generation. */
     protected $whitelistfolders = [
-        'classes/local',
+        'classes',
     ];
 };
index 46e3c27..dd68d91 100644 (file)
@@ -94,3 +94,4 @@ $string['templatepickownform'] = 'Use this form as a template';
 $string['templatetypeown'] = 'Own form';
 $string['templatetypeshared'] = 'Shared template';
 $string['templatesource'] = 'Location: {$a->component} ({$a->area})';
+$string['error:notinrange'] = 'Invalid grade \'{$a->grade}\' provided. Grades must be between 0 and {$a->maxgrade}.';
index 8b86b52..ef19298 100644 (file)
@@ -24,6 +24,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+global $CFG;
+
 /** Include essential files */
 require_once($CFG->libdir . '/grade/constants.php');
 
diff --git a/mod/forum/classes/grades/forum_gradeitem.php b/mod/forum/classes/grades/forum_gradeitem.php
new file mode 100644 (file)
index 0000000..37f477d
--- /dev/null
@@ -0,0 +1,231 @@
+<?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/>.
+
+/**
+ * Grade item storage for mod_forum.
+ *
+ * @package   mod_forum
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace mod_forum\grades;
+
+use coding_exception;
+use context;
+use core_grades\component_gradeitem;
+use core_grades\component_gradeitems;
+use gradingform_instance;
+use mod_forum\local\container as forum_container;
+use mod_forum\local\entities\forum as forum_entity;
+use required_capability_exception;
+use stdClass;
+
+/**
+ * Grade item storage for mod_forum.
+ *
+ * @package   mod_forum
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class forum_gradeitem extends component_gradeitem {
+    /** @var forum_entity The forum entity being graded */
+    protected $forum;
+
+    /**
+     * Return an instance based on the context in which it is used.
+     *
+     * @param context $context
+     */
+    public static function load_from_context(context $context): parent {
+        // Get all the factories that are required.
+        $vaultfactory = forum_container::get_vault_factory();
+        $forumvault = $vaultfactory->get_forum_vault();
+
+        $forum = $forumvault->get_from_course_module_id((int) $context->instanceid);
+
+        return static::load_from_forum_entity($forum);
+    }
+
+    /**
+     * Return an instance using the forum_entity instance.
+     *
+     * @param context $context
+     */
+    public static function load_from_forum_entity(forum_entity $forum): self {
+        $instance = new static('mod_forum', $forum->get_context(), 'forum');
+        $instance->forum = $forum;
+
+        return $instance;
+    }
+
+    /**
+     * The table name used for grading.
+     *
+     * @return string
+     */
+    protected function get_table_name(): string {
+        return 'forum_grades';
+    }
+
+    /**
+     * Whether grading is enabled for this item.
+     *
+     * @return bool
+     */
+    public function is_grading_enabled(): bool {
+        return $this->forum->get_grade_for_forum() !== 0;
+    }
+
+    /**
+     * Whether the grader can grade the gradee.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @return bool
+     */
+    public function user_can_grade(stdClass $gradeduser, stdClass $grader): bool {
+        // Validate the required capabilities.
+        $managerfactory = forum_container::get_manager_factory();
+        $capabilitymanager = $managerfactory->get_capability_manager($this->forum);
+
+        return $capabilitymanager->can_grade($grader, $gradeduser);
+    }
+
+    /**
+     * Require that the user can grade, throwing an exception if not.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @throws required_capability_exception
+     */
+    public function require_user_can_grade(stdClass $gradeduser, stdClass $grader): void {
+        if (!$this->user_can_grade($gradeduser, $grader)) {
+            throw new required_capability_exception($this->forum->get_context(), 'mod/forum:grade', 'nopermissions', '');
+        }
+    }
+
+    /**
+     * Get the grade value for this instance.
+     * The itemname is translated to the relevant grade field on the forum entity.
+     *
+     * @param string $itemname
+     * @return int
+     */
+    protected function get_gradeitem_value(): int {
+        $getter = "get_grade_for_{$this->itemname}";
+
+        return $this->forum->{$getter}();
+    }
+
+    /**
+     * Create an empty forum_grade for the specified user and grader.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @return stdClass The newly created grade record
+     */
+    public function create_empty_grade(stdClass $gradeduser, stdClass $grader): stdClass {
+        global $DB;
+
+        $grade = (object) [
+            'forum' => $this->forum->get_id(),
+            'itemnumber' => $this->itemnumber,
+            'userid' => $gradeduser->id,
+            'timemodified' => time(),
+        ];
+        $grade->timecreated = $grade->timemodified;
+
+        $gradeid = $DB->insert_record($this->get_table_name(), $grade);
+
+        return $DB->get_record($this->get_table_name(), ['id' => $gradeid]);
+    }
+
+    /**
+     * Get the grade for the specified user.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @return stdClass The grade value
+     */
+    public function get_grade_for_user(stdClass $gradeduser, stdClass $grader = null): ?stdClass {
+        global $DB;
+
+        $params = [
+            'forum' => $this->forum->get_id(),
+            'itemnumber' => $this->itemnumber,
+            'userid' => $gradeduser->id,
+        ];
+
+        $grade = $DB->get_record($this->get_table_name(), $params);
+
+        if (empty($grade)) {
+            $grade = $this->create_empty_grade($gradeduser, $grader);
+        }
+
+        return $grade ?: null;
+    }
+
+    /**
+     * Get grades for all users for the specified gradeitem.
+     *
+     * @param int $itemnumber The specific grade item to fetch for the user
+     * @return stdClass[] The grades
+     */
+    public function get_all_grades(): array {
+        global $DB;
+
+        return $DB->get_records($this->get_table_name(), [
+            'forum' => $this->forum->get_id(),
+            'itemnumber' => $this->itemnumber,
+        ]);
+    }
+
+    /**
+     * Create or update the grade.
+     *
+     * @param stdClass $grade
+     * @return bool Success
+     */
+    protected function store_grade(stdClass $grade): bool {
+        global $CFG, $DB;
+        require_once("{$CFG->dirroot}/mod/forum/lib.php");
+
+        if ($grade->forum != $this->forum->get_id()) {
+            throw new coding_exception('Incorrect forum provided for this grade');
+        }
+
+        if ($grade->itemnumber != $this->itemnumber) {
+            throw new coding_exception('Incorrect itemnumber provided for this grade');
+        }
+
+        // Ensure that the grade is valid.
+        $this->check_grade_validity($grade->grade);
+
+        $grade->forum = $this->forum->get_id();
+        $grade->timemodified = time();
+
+        $DB->update_record($this->get_table_name(), $grade);
+
+        // Update in the gradebook.
+        $mapper = forum_container::get_legacy_data_mapper_factory()->get_forum_data_mapper();
+        forum_update_grades($mapper->to_legacy_object($this->forum), $grade->userid);
+
+        return true;
+    }
+}
index eda4a63..013dc02 100644 (file)
@@ -736,11 +736,12 @@ function forum_print_recent_activity($course, $viewfullnames, $timestart) {
 function forum_update_grades($forum, $userid = 0): void {
     global $CFG, $DB;
     require_once($CFG->libdir.'/gradelib.php');
+    $cm = get_coursemodule_from_instance('forum', $forum->id);
+    $forum->cmidnumber = $cm->idnumber;
 
     $ratings = null;
     if ($forum->assessed) {
         require_once($CFG->dirroot.'/rating/lib.php');
-        $cm = get_coursemodule_from_instance('forum', $forum->id);
 
         $rm = new rating_manager();
         $ratings = $rm->get_user_grades((object) [
@@ -760,8 +761,35 @@ function forum_update_grades($forum, $userid = 0): void {
 
     $forumgrades = null;
     if ($forum->grade_forum) {
-        // TODO MDL-66080.
-        // Need to create a new table for forum_grades with userid, forumid, rawgrade, etc.
+        $sql = <<<EOF
+SELECT
+    g.userid,
+    0 as datesubmitted,
+    g.grade as rawgrade,
+    g.timemodified as dategraded
+  FROM {forum} f
+  JOIN {forum_grades} g ON g.forum = f.id
+ WHERE f.id = :forumid
+EOF;
+
+        $params = [
+            'forumid' => $forum->id,
+        ];
+
+        if ($userid) {
+            $sql .= "AND g.userid = :userid";
+            $params['userid'] = $userid;
+        }
+
+        $forumgrades = [];
+        if ($grades = $DB->get_recordset_sql($sql, $params)) {
+            foreach ($grades as $userid => $grade) {
+                if ($grade->rawgrade != -1) {
+                    $forumgrades[$userid] = $grade;
+                }
+            }
+            $grades->close();
+        }
     }
 
     forum_grade_item_update($forum, $ratings, $forumgrades);
index c8e9d8e..527c7e7 100644 (file)
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
 return new class extends phpunit_coverage_info {
     /** @var array The list of folders relative to the plugin root to whitelist in coverage generation. */
     protected $whitelistfolders = [
-        'classes/local',
+        'classes',
         'externallib.php',
     ];
 
diff --git a/mod/forum/tests/grades_forum_gradeitem_test.php b/mod/forum/tests/grades_forum_gradeitem_test.php
new file mode 100644 (file)
index 0000000..68c5c82
--- /dev/null
@@ -0,0 +1,163 @@
+<?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 the the Forum gradeitem.
+ *
+ * @package    mod_forum
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tests\mod_forum\grades;
+
+use core_grades\component_gradeitem;
+use mod_forum\grades\forum_gradeitem as gradeitem;
+use mod_forum\local\entities\forum as forum_entity;
+use gradingform_controller;
+use mod_forum\grades\forum_gradeitem;
+
+require_once(__DIR__ . '/generator_trait.php');
+
+/**
+ * Tests for the the Forum gradeitem.
+ *
+ * @package    mod_forum
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class forum_gradeitem_test extends \advanced_testcase {
+    use \mod_forum_tests_generator_trait;
+
+    /**
+     * Test fetching of a grade for a user when the grade has been created.
+     */
+    public function test_get_grade_for_user_exists(): void {
+        $forum = $this->get_forum_instance([
+            'grade_forum' => 0,
+        ]);
+        $course = $forum->get_course_record();
+        [$student] = $this->helper_create_users($course, 1);
+        [$grader] = $this->helper_create_users($course, 1, 'editingteacher');
+
+        $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+        // Create the grade record.
+        $grade = $gradeitem->create_empty_grade($student, $grader);
+
+        $this->assertIsObject($grade);
+        $this->assertEquals($student->id, $grade->userid);
+    }
+
+    /**
+     * Ensure that it is possible to get, and update, a grade for a user when simple direct grading is in use.
+     */
+    public function test_get_and_store_grade_for_user_with_simple_direct_grade(): void {
+        $forum = $this->get_forum_instance([
+            'grade_forum' => 100,
+        ]);
+        $course = $forum->get_course_record();
+        [$student] = $this->helper_create_users($course, 1);
+        [$grader] = $this->helper_create_users($course, 1, 'editingteacher');
+
+        $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+        // Create the grade record.
+        $grade = $gradeitem->create_empty_grade($student, $grader);
+
+        $this->assertIsObject($grade);
+        $this->assertEquals($student->id, $grade->userid);
+
+        // Store a new value.
+        $gradeitem->store_grade_from_formdata($student, $grader, (object) ['grade' => 97]);
+    }
+
+    /**
+     * Ensure that it is possible to get, and update, a grade for a user when a rubric is in use.
+     */
+    public function test_get_and_store_grade_for_user_with_rubric(): void {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $generator = \testing_util::get_data_generator();
+        $gradinggenerator = $generator->get_plugin_generator('core_grading');
+        $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
+
+        $forum = $this->get_forum_instance([
+            'grade_forum' => 100,
+        ]);
+        $course = $forum->get_course_record();
+        $context = $forum->get_context();
+        [$student] = $this->helper_create_users($course, 1);
+        [$grader] = $this->helper_create_users($course, 1, 'editingteacher');
+        [$editor] = $this->helper_create_users($course, 1, 'editingteacher');
+
+        // Note: This must be run as a user because it messes with file uploads and drafts.
+        $this->setUser($editor);
+
+        $controller = $rubricgenerator->get_test_rubric($context, 'mod_forum', 'forum');
+
+        // Create the forum_gradeitem object that we'll be testing.
+        $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+        // The current grade should be null initially.
+        $this->assertCount(0, $DB->get_records('forum_grades'));
+        $grade = $gradeitem->get_grade_for_user($student, $grader);
+        $instance = $gradeitem->get_advanced_grading_instance($grader, $grade);
+
+        $this->assertIsObject($grade);
+        $this->assertEquals($student->id, $grade->userid);
+        $this->assertEquals($forum->get_id(), $grade->forum);
+
+        $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
+        $data = $rubricgenerator->get_submitted_form_data($controller, $grade->id, [
+            'Spelling is important' => [
+                'score' => 2,
+                'remark' => 'Abracadabra',
+            ],
+            'Pictures' => [
+                'score' => 1,
+                'remark' => 'More than one picture',
+            ],
+        ]);
+
+        // Store a new value.
+        $gradeitem->store_grade_from_formdata($student, $grader, (object) [
+            'instanceid' => $instance->get_id(),
+            'advancedgrading' => $data,
+        ]);
+    }
+
+    /**
+     * Get a forum instance.
+     *
+     * @param array $config
+     * @return forum_entity
+     */
+    protected function get_forum_instance(array $config = []): forum_entity {
+        $this->resetAfterTest();
+
+        $datagenerator = $this->getDataGenerator();
+        $course = $datagenerator->create_course();
+        $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id]));
+
+        $vaultfactory = \mod_forum\local\container::get_vault_factory();
+        $vault = $vaultfactory->get_forum_vault();
+
+        return $vault->get_from_id((int) $forum->id);
+    }
+}
index 65fbcee..40df165 100644 (file)
@@ -42,13 +42,56 @@ use coding_exception;
 class gradeitems_test extends advanced_testcase {
 
     /**
-     * Ensure that a component which does not implement the mapping class excepts.
+     * Ensure that the mappings are present and correct.
      */
-    public function test_get_mappings() {
-        $mappings = component_gradeitems::get_mappings_for_component('mod_forum');
+    public function test_get_itemname_mapping_for_component(): void {
+        $mappings = component_gradeitems::get_itemname_mapping_for_component('mod_forum');
         $this->assertIsArray($mappings);
         $this->assertCount(2, $mappings);
         $this->assertArraySubset([0 => 'rating'], $mappings);
         $this->assertArraySubset([1 => 'forum'], $mappings);
     }
+
+    /**
+     * Ensure that the advanced grading only applies to the relevant items.
+     */
+    public function test_get_advancedgrading_itemnames_for_component(): void {
+        $mappings = component_gradeitems::get_advancedgrading_itemnames_for_component('mod_forum');
+        $this->assertIsArray($mappings);
+        $this->assertCount(1, $mappings);
+        $this->assertContains('forum', $mappings);
+        $this->assertNotContains('rating', $mappings);
+    }
+
+    /**
+     * Ensure that the correct items are identified by is_advancedgrading_itemname.
+     *
+     * @dataProvider is_advancedgrading_itemname_provider
+     * @param string $itemname
+     * @param bool $expected
+     */
+    public function test_is_advancedgrading_itemname(string $itemname, bool $expected): void {
+        $this->assertEquals(
+            $expected,
+            component_gradeitems::is_advancedgrading_itemname('mod_forum', $itemname)
+        );
+    }
+
+    /**
+     * Data provider for tests of is_advancedgrading_itemname.
+     *
+     * @return array
+     */
+    public function is_advancedgrading_itemname_provider(): array {
+        return [
+            'rating is not advanced' => [
+                'rating',
+                false,
+            ],
+            'Whole forum grading is advanced' => [
+                'forum',
+                true,
+            ],
+        ];
+    }
 }