You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1244 lines
46 KiB
1244 lines
46 KiB
<?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 a class to represent an individual user's grade
|
|
*
|
|
* @package core_grades
|
|
* @category grade
|
|
* @copyright 2006 Nicolas Connault
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
require_once('grade_object.php');
|
|
|
|
/**
|
|
* grade_grades is an object mapped to DB table {prefix}grade_grades
|
|
*
|
|
* @package core_grades
|
|
* @category grade
|
|
* @copyright 2006 Nicolas Connault
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
class grade_grade extends grade_object {
|
|
|
|
/**
|
|
* The DB table.
|
|
* @var string $table
|
|
*/
|
|
public $table = 'grade_grades';
|
|
|
|
/**
|
|
* Array of required table fields, must start with 'id'.
|
|
* @var array $required_fields
|
|
*/
|
|
public $required_fields = array('id', 'itemid', 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin',
|
|
'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked',
|
|
'locktime', 'exported', 'overridden', 'excluded', 'timecreated',
|
|
'timemodified', 'aggregationstatus', 'aggregationweight');
|
|
|
|
/**
|
|
* Array of optional fields with default values (these should match db defaults)
|
|
* @var array $optional_fields
|
|
*/
|
|
public $optional_fields = array('feedback'=>null, 'feedbackformat'=>0, 'information'=>null, 'informationformat'=>0);
|
|
|
|
/**
|
|
* The id of the grade_item this grade belongs to.
|
|
* @var int $itemid
|
|
*/
|
|
public $itemid;
|
|
|
|
/**
|
|
* The grade_item object referenced by $this->itemid.
|
|
* @var grade_item $grade_item
|
|
*/
|
|
public $grade_item;
|
|
|
|
/**
|
|
* The id of the user this grade belongs to.
|
|
* @var int $userid
|
|
*/
|
|
public $userid;
|
|
|
|
/**
|
|
* The grade value of this raw grade, if such was provided by the module.
|
|
* @var float $rawgrade
|
|
*/
|
|
public $rawgrade;
|
|
|
|
/**
|
|
* The maximum allowable grade when this grade was created.
|
|
* @var float $rawgrademax
|
|
*/
|
|
public $rawgrademax = 100;
|
|
|
|
/**
|
|
* The minimum allowable grade when this grade was created.
|
|
* @var float $rawgrademin
|
|
*/
|
|
public $rawgrademin = 0;
|
|
|
|
/**
|
|
* id of the scale, if this grade is based on a scale.
|
|
* @var int $rawscaleid
|
|
*/
|
|
public $rawscaleid;
|
|
|
|
/**
|
|
* The userid of the person who last modified this grade.
|
|
* @var int $usermodified
|
|
*/
|
|
public $usermodified;
|
|
|
|
/**
|
|
* The final value of this grade.
|
|
* @var float $finalgrade
|
|
*/
|
|
public $finalgrade;
|
|
|
|
/**
|
|
* 0 if visible, 1 always hidden or date not visible until
|
|
* @var float $hidden
|
|
*/
|
|
public $hidden = 0;
|
|
|
|
/**
|
|
* 0 not locked, date when the item was locked
|
|
* @var float locked
|
|
*/
|
|
public $locked = 0;
|
|
|
|
/**
|
|
* 0 no automatic locking, date when to lock the grade automatically
|
|
* @var float $locktime
|
|
*/
|
|
public $locktime = 0;
|
|
|
|
/**
|
|
* Exported flag
|
|
* @var bool $exported
|
|
*/
|
|
public $exported = 0;
|
|
|
|
/**
|
|
* Overridden flag
|
|
* @var bool $overridden
|
|
*/
|
|
public $overridden = 0;
|
|
|
|
/**
|
|
* Grade excluded from aggregation functions
|
|
* @var bool $excluded
|
|
*/
|
|
public $excluded = 0;
|
|
|
|
/**
|
|
* TODO: HACK: create a new field datesubmitted - the date of submission if any (MDL-31377)
|
|
* @var bool $timecreated
|
|
*/
|
|
public $timecreated = null;
|
|
|
|
/**
|
|
* TODO: HACK: create a new field dategraded - the date of grading (MDL-31378)
|
|
* @var bool $timemodified
|
|
*/
|
|
public $timemodified = null;
|
|
|
|
/**
|
|
* Aggregation status flag. Can be one of 'unknown', 'dropped', 'novalue' or 'used'.
|
|
* @var string $aggregationstatus
|
|
*/
|
|
public $aggregationstatus = 'unknown';
|
|
|
|
/**
|
|
* Aggregation weight is the specific weight used in the aggregation calculation for this grade.
|
|
* @var float $aggregationweight
|
|
*/
|
|
public $aggregationweight = null;
|
|
|
|
/**
|
|
* Feedback files to copy.
|
|
*
|
|
* Example -
|
|
*
|
|
* [
|
|
* 'contextid' => 1,
|
|
* 'component' => 'mod_xyz',
|
|
* 'filearea' => 'mod_xyz_feedback',
|
|
* 'itemid' => 2
|
|
* ];
|
|
*
|
|
* @var array
|
|
*/
|
|
public $feedbackfiles = [];
|
|
|
|
/**
|
|
* Returns array of grades for given grade_item+users
|
|
*
|
|
* @param grade_item $grade_item
|
|
* @param array $userids
|
|
* @param bool $include_missing include grades that do not exist yet
|
|
* @return array userid=>grade_grade array
|
|
*/
|
|
public static function fetch_users_grades($grade_item, $userids, $include_missing=true) {
|
|
global $DB;
|
|
|
|
// hmm, there might be a problem with length of sql query
|
|
// if there are too many users requested - we might run out of memory anyway
|
|
$limit = 2000;
|
|
$count = count($userids);
|
|
if ($count > $limit) {
|
|
$half = (int)($count/2);
|
|
$first = array_slice($userids, 0, $half);
|
|
$second = array_slice($userids, $half);
|
|
return grade_grade::fetch_users_grades($grade_item, $first, $include_missing) + grade_grade::fetch_users_grades($grade_item, $second, $include_missing);
|
|
}
|
|
|
|
list($user_ids_cvs, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uid0');
|
|
$params['giid'] = $grade_item->id;
|
|
$result = array();
|
|
if ($grade_records = $DB->get_records_select('grade_grades', "itemid=:giid AND userid $user_ids_cvs", $params)) {
|
|
foreach ($grade_records as $record) {
|
|
$result[$record->userid] = new grade_grade($record, false);
|
|
}
|
|
}
|
|
if ($include_missing) {
|
|
foreach ($userids as $userid) {
|
|
if (!array_key_exists($userid, $result)) {
|
|
$grade_grade = new grade_grade();
|
|
$grade_grade->userid = $userid;
|
|
$grade_grade->itemid = $grade_item->id;
|
|
$result[$userid] = $grade_grade;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Loads the grade_item object referenced by $this->itemid and saves it as $this->grade_item for easy access
|
|
*
|
|
* @return grade_item The grade_item instance referenced by $this->itemid
|
|
*/
|
|
public function load_grade_item() {
|
|
if (empty($this->itemid)) {
|
|
debugging('Missing itemid');
|
|
$this->grade_item = null;
|
|
return null;
|
|
}
|
|
|
|
if (empty($this->grade_item)) {
|
|
$this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
|
|
|
|
} else if ($this->grade_item->id != $this->itemid) {
|
|
debugging('Itemid mismatch');
|
|
$this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
|
|
}
|
|
|
|
return $this->grade_item;
|
|
}
|
|
|
|
/**
|
|
* Is grading object editable?
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_editable() {
|
|
if ($this->is_locked()) {
|
|
return false;
|
|
}
|
|
|
|
$grade_item = $this->load_grade_item();
|
|
|
|
if ($grade_item->gradetype == GRADE_TYPE_NONE) {
|
|
return false;
|
|
}
|
|
|
|
if ($grade_item->is_course_item() or $grade_item->is_category_item()) {
|
|
return (bool)get_config('moodle', 'grade_overridecat');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check grade lock status. Uses both grade item lock and grade lock.
|
|
* Internally any date in locked field (including future ones) means locked,
|
|
* the date is stored for logging purposes only.
|
|
*
|
|
* @return bool True if locked, false if not
|
|
*/
|
|
public function is_locked() {
|
|
$this->load_grade_item();
|
|
if (empty($this->grade_item)) {
|
|
return !empty($this->locked);
|
|
} else {
|
|
return !empty($this->locked) or $this->grade_item->is_locked();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if grade overridden
|
|
*
|
|
* @return bool True if grade is overriden
|
|
*/
|
|
public function is_overridden() {
|
|
return !empty($this->overridden);
|
|
}
|
|
|
|
/**
|
|
* Returns timestamp of submission related to this grade, null if not submitted.
|
|
*
|
|
* @return int Timestamp
|
|
*/
|
|
public function get_datesubmitted() {
|
|
//TODO: HACK - create new fields (MDL-31379)
|
|
return $this->timecreated;
|
|
}
|
|
|
|
/**
|
|
* Returns the weight this grade contributed to the aggregated grade
|
|
*
|
|
* @return float|null
|
|
*/
|
|
public function get_aggregationweight() {
|
|
return $this->aggregationweight;
|
|
}
|
|
|
|
/**
|
|
* Set aggregationweight.
|
|
*
|
|
* @param float $aggregationweight
|
|
* @return void
|
|
*/
|
|
public function set_aggregationweight($aggregationweight) {
|
|
$this->aggregationweight = $aggregationweight;
|
|
$this->update();
|
|
}
|
|
|
|
/**
|
|
* Returns the info on how this value was used in the aggregated grade
|
|
*
|
|
* @return string One of 'dropped', 'excluded', 'novalue', 'used' or 'extra'
|
|
*/
|
|
public function get_aggregationstatus() {
|
|
return $this->aggregationstatus;
|
|
}
|
|
|
|
/**
|
|
* Set aggregationstatus flag
|
|
*
|
|
* @param string $aggregationstatus
|
|
* @return void
|
|
*/
|
|
public function set_aggregationstatus($aggregationstatus) {
|
|
$this->aggregationstatus = $aggregationstatus;
|
|
$this->update();
|
|
}
|
|
|
|
/**
|
|
* Returns the minimum and maximum number of points this grade is graded with respect to.
|
|
*
|
|
* @since Moodle 2.8.7, 2.9.1
|
|
* @return array A list containing, in order, the minimum and maximum number of points.
|
|
*/
|
|
protected function get_grade_min_and_max() {
|
|
global $CFG;
|
|
$this->load_grade_item();
|
|
|
|
// When the following setting is turned on we use the grade_grade raw min and max values.
|
|
$minmaxtouse = grade_get_setting($this->grade_item->courseid, 'minmaxtouse', $CFG->grade_minmaxtouse);
|
|
|
|
// Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
|
|
// wish to update the grades.
|
|
$gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $this->grade_item->courseid;
|
|
// Gradebook is frozen, run through old code.
|
|
if (isset($CFG->$gradebookcalculationsfreeze) && (int)$CFG->$gradebookcalculationsfreeze <= 20150627) {
|
|
// Only aggregate items use separate min grades.
|
|
if ($minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE || $this->grade_item->is_aggregate_item()) {
|
|
return array($this->rawgrademin, $this->rawgrademax);
|
|
} else {
|
|
return array($this->grade_item->grademin, $this->grade_item->grademax);
|
|
}
|
|
} else {
|
|
// Only aggregate items use separate min grades, unless they are calculated grade items.
|
|
if (($this->grade_item->is_aggregate_item() && !$this->grade_item->is_calculated())
|
|
|| $minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE) {
|
|
return array($this->rawgrademin, $this->rawgrademax);
|
|
} else {
|
|
return array($this->grade_item->grademin, $this->grade_item->grademax);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the minimum number of points this grade is graded with.
|
|
*
|
|
* @since Moodle 2.8.7, 2.9.1
|
|
* @return float The minimum number of points
|
|
*/
|
|
public function get_grade_min() {
|
|
list($min, $max) = $this->get_grade_min_and_max();
|
|
|
|
return $min;
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum number of points this grade is graded with respect to.
|
|
*
|
|
* @since Moodle 2.8.7, 2.9.1
|
|
* @return float The maximum number of points
|
|
*/
|
|
public function get_grade_max() {
|
|
list($min, $max) = $this->get_grade_min_and_max();
|
|
|
|
return $max;
|
|
}
|
|
|
|
/**
|
|
* Returns timestamp when last graded, null if no grade present
|
|
*
|
|
* @return int
|
|
*/
|
|
public function get_dategraded() {
|
|
//TODO: HACK - create new fields (MDL-31379)
|
|
if (is_null($this->finalgrade) and is_null($this->feedback)) {
|
|
return null; // no grade == no date
|
|
} else if ($this->overridden) {
|
|
return $this->overridden;
|
|
} else {
|
|
return $this->timemodified;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the overridden status of grade
|
|
*
|
|
* @param bool $state requested overridden state
|
|
* @param bool $refresh refresh grades from external activities if needed
|
|
* @return bool true is db state changed
|
|
*/
|
|
public function set_overridden($state, $refresh = true) {
|
|
if (empty($this->overridden) and $state) {
|
|
$this->overridden = time();
|
|
$this->update();
|
|
return true;
|
|
|
|
} else if (!empty($this->overridden) and !$state) {
|
|
$this->overridden = 0;
|
|
$this->update();
|
|
|
|
if ($refresh) {
|
|
//refresh when unlocking
|
|
$this->grade_item->refresh_grades($this->userid);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks if grade excluded from aggregation functions
|
|
*
|
|
* @return bool True if grade is excluded from aggregation
|
|
*/
|
|
public function is_excluded() {
|
|
return !empty($this->excluded);
|
|
}
|
|
|
|
/**
|
|
* Set the excluded status of grade
|
|
*
|
|
* @param bool $state requested excluded state
|
|
* @return bool True is database state changed
|
|
*/
|
|
public function set_excluded($state) {
|
|
if (empty($this->excluded) and $state) {
|
|
$this->excluded = time();
|
|
$this->update();
|
|
return true;
|
|
|
|
} else if (!empty($this->excluded) and !$state) {
|
|
$this->excluded = 0;
|
|
$this->update();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Lock/unlock this grade.
|
|
*
|
|
* @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
|
|
* @param bool $cascade Ignored param
|
|
* @param bool $refresh Refresh grades when unlocking
|
|
* @return bool True if successful, false if can not set new lock state for grade
|
|
*/
|
|
public function set_locked($lockedstate, $cascade=false, $refresh=true) {
|
|
$this->load_grade_item();
|
|
|
|
if ($lockedstate) {
|
|
if ($this->grade_item->needsupdate) {
|
|
//can not lock grade if final not calculated!
|
|
return false;
|
|
}
|
|
|
|
$this->locked = time();
|
|
$this->update();
|
|
|
|
return true;
|
|
|
|
} else {
|
|
if (!empty($this->locked) and $this->locktime < time()) {
|
|
//we have to reset locktime or else it would lock up again
|
|
$this->locktime = 0;
|
|
}
|
|
|
|
// remove the locked flag
|
|
$this->locked = 0;
|
|
$this->update();
|
|
|
|
if ($refresh and !$this->is_overridden()) {
|
|
//refresh when unlocking and not overridden
|
|
$this->grade_item->refresh_grades($this->userid);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lock the grade if needed. Make sure this is called only when final grades are valid
|
|
*
|
|
* @param array $items array of all grade item ids
|
|
* @return void
|
|
*/
|
|
public static function check_locktime_all($items) {
|
|
global $CFG, $DB;
|
|
|
|
$now = time(); // no rounding needed, this is not supposed to be called every 10 seconds
|
|
list($usql, $params) = $DB->get_in_or_equal($items);
|
|
$params[] = $now;
|
|
$rs = $DB->get_recordset_select('grade_grades', "itemid $usql AND locked = 0 AND locktime > 0 AND locktime < ?", $params);
|
|
foreach ($rs as $grade) {
|
|
$grade_grade = new grade_grade($grade, false);
|
|
$grade_grade->locked = time();
|
|
$grade_grade->update('locktime');
|
|
}
|
|
$rs->close();
|
|
}
|
|
|
|
/**
|
|
* Set the locktime for this grade.
|
|
*
|
|
* @param int $locktime timestamp for lock to activate
|
|
* @return void
|
|
*/
|
|
public function set_locktime($locktime) {
|
|
$this->locktime = $locktime;
|
|
$this->update();
|
|
}
|
|
|
|
/**
|
|
* Get the locktime for this grade.
|
|
*
|
|
* @return int $locktime timestamp for lock to activate
|
|
*/
|
|
public function get_locktime() {
|
|
$this->load_grade_item();
|
|
|
|
$item_locktime = $this->grade_item->get_locktime();
|
|
|
|
if (empty($this->locktime) or ($item_locktime and $item_locktime < $this->locktime)) {
|
|
return $item_locktime;
|
|
|
|
} else {
|
|
return $this->locktime;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check grade hidden status. Uses data from both grade item and grade.
|
|
*
|
|
* @return bool true if hidden, false if not
|
|
*/
|
|
public function is_hidden() {
|
|
$this->load_grade_item();
|
|
if (empty($this->grade_item)) {
|
|
return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time());
|
|
} else {
|
|
return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()) or $this->grade_item->is_hidden();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check grade hidden status. Uses data from both grade item and grade.
|
|
*
|
|
* @return bool true if hiddenuntil, false if not
|
|
*/
|
|
public function is_hiddenuntil() {
|
|
$this->load_grade_item();
|
|
|
|
if ($this->hidden == 1 or $this->grade_item->hidden == 1) {
|
|
return false; //always hidden
|
|
}
|
|
|
|
if ($this->hidden > 1 or $this->grade_item->hidden > 1) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check grade hidden status. Uses data from both grade item and grade.
|
|
*
|
|
* @return int 0 means visible, 1 hidden always, timestamp hidden until
|
|
*/
|
|
public function get_hidden() {
|
|
$this->load_grade_item();
|
|
|
|
$item_hidden = $this->grade_item->get_hidden();
|
|
|
|
if ($item_hidden == 1) {
|
|
return 1;
|
|
|
|
} else if ($item_hidden == 0) {
|
|
return $this->hidden;
|
|
|
|
} else {
|
|
if ($this->hidden == 0) {
|
|
return $item_hidden;
|
|
} else if ($this->hidden == 1) {
|
|
return 1;
|
|
} else if ($this->hidden > $item_hidden) {
|
|
return $this->hidden;
|
|
} else {
|
|
return $item_hidden;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the hidden status of grade, 0 mean visible, 1 always hidden, number means date to hide until.
|
|
*
|
|
* @param int $hidden new hidden status
|
|
* @param bool $cascade ignored
|
|
*/
|
|
public function set_hidden($hidden, $cascade=false) {
|
|
$this->hidden = $hidden;
|
|
$this->update();
|
|
}
|
|
|
|
/**
|
|
* Finds and returns a grade_grade instance based on params.
|
|
*
|
|
* @param array $params associative arrays varname=>value
|
|
* @return grade_grade Returns a grade_grade instance or false if none found
|
|
*/
|
|
public static function fetch($params) {
|
|
return grade_object::fetch_helper('grade_grades', 'grade_grade', $params);
|
|
}
|
|
|
|
/**
|
|
* Finds and returns all grade_grade instances based on params.
|
|
*
|
|
* @param array $params associative arrays varname=>value
|
|
* @return array array of grade_grade instances or false if none found.
|
|
*/
|
|
public static function fetch_all($params) {
|
|
return grade_object::fetch_all_helper('grade_grades', 'grade_grade', $params);
|
|
}
|
|
|
|
/**
|
|
* Given a float value situated between a source minimum and a source maximum, converts it to the
|
|
* corresponding value situated between a target minimum and a target maximum. Thanks to Darlene
|
|
* for the formula :-)
|
|
*
|
|
* @param float $rawgrade
|
|
* @param float $source_min
|
|
* @param float $source_max
|
|
* @param float $target_min
|
|
* @param float $target_max
|
|
* @return float Converted value
|
|
*/
|
|
public static function standardise_score($rawgrade, $source_min, $source_max, $target_min, $target_max) {
|
|
if (is_null($rawgrade)) {
|
|
return null;
|
|
}
|
|
|
|
if ($source_max == $source_min or $target_min == $target_max) {
|
|
// prevent division by 0
|
|
return $target_max;
|
|
}
|
|
|
|
$factor = ($rawgrade - $source_min) / ($source_max - $source_min);
|
|
$diff = $target_max - $target_min;
|
|
$standardised_value = $factor * $diff + $target_min;
|
|
return $standardised_value;
|
|
}
|
|
|
|
/**
|
|
* Given an array like this:
|
|
* $a = array(1=>array(2, 3),
|
|
* 2=>array(4),
|
|
* 3=>array(1),
|
|
* 4=>array())
|
|
* this function fully resolves the dependencies so each value will be an array of
|
|
* the all items this item depends on and their dependencies (and their dependencies...).
|
|
* It should not explode if there are circular dependencies.
|
|
* The dependency depth array will list the number of branches in the tree above each leaf.
|
|
*
|
|
* @param array $dependson Array to flatten
|
|
* @param array $dependencydepth Array of itemids => depth. Initially these should be all set to 1.
|
|
* @return array Flattened array
|
|
*/
|
|
protected static function flatten_dependencies_array(&$dependson, &$dependencydepth) {
|
|
// Flatten the nested dependencies - this will handle recursion bombs because it removes duplicates.
|
|
$somethingchanged = true;
|
|
while ($somethingchanged) {
|
|
$somethingchanged = false;
|
|
|
|
foreach ($dependson as $itemid => $depends) {
|
|
// Make a copy so we can tell if it changed.
|
|
$before = $dependson[$itemid];
|
|
foreach ($depends as $subitemid => $subdepends) {
|
|
$dependson[$itemid] = array_unique(array_merge($depends, $dependson[$subdepends]));
|
|
sort($dependson[$itemid], SORT_NUMERIC);
|
|
}
|
|
if ($before != $dependson[$itemid]) {
|
|
$somethingchanged = true;
|
|
if (!isset($dependencydepth[$itemid])) {
|
|
$dependencydepth[$itemid] = 1;
|
|
} else {
|
|
$dependencydepth[$itemid]++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return array of grade item ids that are either hidden or indirectly depend
|
|
* on hidden grades, excluded grades are not returned.
|
|
* THIS IS A REALLY BIG HACK! to be replaced by conditional aggregation of hidden grades in 2.0
|
|
*
|
|
* @param array $grade_grades all course grades of one user, & used for better internal caching
|
|
* @param array $grade_items array of grade items, & used for better internal caching
|
|
* @return array This is an array of following arrays:
|
|
* unknown => list of item ids that may be affected by hiding (with the ITEM ID as both the key and the value) - for BC with old gradereport plugins
|
|
* unknowngrades => list of item ids that may be affected by hiding (with the calculated grade as the value)
|
|
* altered => list of item ids that are definitely affected by hiding (with the calculated grade as the value)
|
|
* alteredgrademax => for each item in altered or unknown, the new value of the grademax
|
|
* alteredgrademin => for each item in altered or unknown, the new value of the grademin
|
|
* alteredgradestatus => for each item with a modified status - the value of the new status
|
|
* alteredgradeweight => for each item with a modified weight - the value of the new weight
|
|
*/
|
|
public static function get_hiding_affected(&$grade_grades, &$grade_items) {
|
|
global $CFG;
|
|
|
|
if (count($grade_grades) !== count($grade_items)) {
|
|
print_error('invalidarraysize', 'debug', '', 'grade_grade::get_hiding_affected()!');
|
|
}
|
|
|
|
$dependson = array();
|
|
$todo = array();
|
|
$unknown = array(); // can not find altered
|
|
$altered = array(); // altered grades
|
|
$alteredgrademax = array(); // Altered grade max values.
|
|
$alteredgrademin = array(); // Altered grade min values.
|
|
$alteredaggregationstatus = array(); // Altered aggregation status.
|
|
$alteredaggregationweight = array(); // Altered aggregation weight.
|
|
$dependencydepth = array();
|
|
|
|
$hiddenfound = false;
|
|
foreach($grade_grades as $itemid=>$unused) {
|
|
$grade_grade =& $grade_grades[$itemid];
|
|
// We need the immediate dependencies of all every grade_item so we can calculate nested dependencies.
|
|
$dependson[$grade_grade->itemid] = $grade_items[$grade_grade->itemid]->depends_on();
|
|
if ($grade_grade->is_excluded()) {
|
|
//nothing to do, aggregation is ok
|
|
} else if ($grade_grade->is_hidden()) {
|
|
$hiddenfound = true;
|
|
$altered[$grade_grade->itemid] = null;
|
|
$alteredaggregationstatus[$grade_grade->itemid] = 'dropped';
|
|
$alteredaggregationweight[$grade_grade->itemid] = 0;
|
|
} else if ($grade_grade->is_locked() or $grade_grade->is_overridden()) {
|
|
// no need to recalculate locked or overridden grades
|
|
} else {
|
|
if (!empty($dependson[$grade_grade->itemid])) {
|
|
$dependencydepth[$grade_grade->itemid] = 1;
|
|
$todo[] = $grade_grade->itemid;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flatten the dependency tree and count number of branches to each leaf.
|
|
self::flatten_dependencies_array($dependson, $dependencydepth);
|
|
|
|
if (!$hiddenfound) {
|
|
return array('unknown' => array(),
|
|
'unknowngrades' => array(),
|
|
'altered' => array(),
|
|
'alteredgrademax' => array(),
|
|
'alteredgrademin' => array(),
|
|
'alteredaggregationstatus' => array(),
|
|
'alteredaggregationweight' => array());
|
|
}
|
|
// This line ensures that $dependencydepth has the same number of items as $todo.
|
|
$dependencydepth = array_intersect_key($dependencydepth, array_flip($todo));
|
|
// We need to resort the todo list by the dependency depth. This guarantees we process the leaves, then the branches.
|
|
array_multisort($dependencydepth, $todo);
|
|
|
|
$max = count($todo);
|
|
$hidden_precursors = null;
|
|
for($i=0; $i<$max; $i++) {
|
|
$found = false;
|
|
foreach($todo as $key=>$do) {
|
|
$hidden_precursors = array_intersect($dependson[$do], array_keys($unknown));
|
|
if ($hidden_precursors) {
|
|
// this item depends on hidden grade indirectly
|
|
$unknown[$do] = $grade_grades[$do]->finalgrade;
|
|
unset($todo[$key]);
|
|
$found = true;
|
|
continue;
|
|
|
|
} else if (!array_intersect($dependson[$do], $todo)) {
|
|
$hidden_precursors = array_intersect($dependson[$do], array_keys($altered));
|
|
// If the dependency is a sum aggregation, we need to process it as if it had hidden items.
|
|
// The reason for this, is that the code will recalculate the maxgrade by removing ungraded
|
|
// items and accounting for 'drop x grades' and then stored back in our virtual grade_items.
|
|
// This recalculation is necessary because there will be a call to:
|
|
// $grade_category->aggregate_values_and_adjust_bounds
|
|
// for the top level grade that will depend on knowing what that caclulated grademax is
|
|
// and it finds that value by checking the virtual grade_items.
|
|
$issumaggregate = false;
|
|
if ($grade_items[$do]->itemtype == 'category') {
|
|
$issumaggregate = $grade_items[$do]->load_item_category()->aggregation == GRADE_AGGREGATE_SUM;
|
|
}
|
|
if (!$hidden_precursors && !$issumaggregate) {
|
|
unset($todo[$key]);
|
|
$found = true;
|
|
continue;
|
|
|
|
} else {
|
|
// depends on altered grades - we should try to recalculate if possible
|
|
if ($grade_items[$do]->is_calculated() or
|
|
(!$grade_items[$do]->is_category_item() and !$grade_items[$do]->is_course_item())
|
|
) {
|
|
// This is a grade item that is not a category or course and has been affected by grade hiding.
|
|
// I guess this means it is a calculation that needs to be recalculated.
|
|
$unknown[$do] = $grade_grades[$do]->finalgrade;
|
|
unset($todo[$key]);
|
|
$found = true;
|
|
continue;
|
|
|
|
} else {
|
|
// This is a grade category (or course).
|
|
$grade_category = $grade_items[$do]->load_item_category();
|
|
|
|
// Build a new list of the grades in this category.
|
|
$values = array();
|
|
$immediatedepends = $grade_items[$do]->depends_on();
|
|
foreach ($immediatedepends as $itemid) {
|
|
if (array_key_exists($itemid, $altered)) {
|
|
//nulling an altered precursor
|
|
$values[$itemid] = $altered[$itemid];
|
|
if (is_null($values[$itemid])) {
|
|
// This means this was a hidden grade item removed from the result.
|
|
unset($values[$itemid]);
|
|
}
|
|
} elseif (empty($values[$itemid])) {
|
|
$values[$itemid] = $grade_grades[$itemid]->finalgrade;
|
|
}
|
|
}
|
|
|
|
foreach ($values as $itemid=>$value) {
|
|
if ($grade_grades[$itemid]->is_excluded()) {
|
|
unset($values[$itemid]);
|
|
$alteredaggregationstatus[$itemid] = 'excluded';
|
|
$alteredaggregationweight[$itemid] = null;
|
|
continue;
|
|
}
|
|
// The grade min/max may have been altered by hiding.
|
|
$grademin = $grade_items[$itemid]->grademin;
|
|
if (isset($alteredgrademin[$itemid])) {
|
|
$grademin = $alteredgrademin[$itemid];
|
|
}
|
|
$grademax = $grade_items[$itemid]->grademax;
|
|
if (isset($alteredgrademax[$itemid])) {
|
|
$grademax = $alteredgrademax[$itemid];
|
|
}
|
|
$values[$itemid] = grade_grade::standardise_score($value, $grademin, $grademax, 0, 1);
|
|
}
|
|
|
|
if ($grade_category->aggregateonlygraded) {
|
|
foreach ($values as $itemid=>$value) {
|
|
if (is_null($value)) {
|
|
unset($values[$itemid]);
|
|
$alteredaggregationstatus[$itemid] = 'novalue';
|
|
$alteredaggregationweight[$itemid] = null;
|
|
}
|
|
}
|
|
} else {
|
|
foreach ($values as $itemid=>$value) {
|
|
if (is_null($value)) {
|
|
$values[$itemid] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// limit and sort
|
|
$allvalues = $values;
|
|
$grade_category->apply_limit_rules($values, $grade_items);
|
|
|
|
$moredropped = array_diff($allvalues, $values);
|
|
foreach ($moredropped as $drop => $unused) {
|
|
$alteredaggregationstatus[$drop] = 'dropped';
|
|
$alteredaggregationweight[$drop] = null;
|
|
}
|
|
|
|
foreach ($values as $itemid => $val) {
|
|
if ($grade_category->is_extracredit_used() && ($grade_items[$itemid]->aggregationcoef > 0)) {
|
|
$alteredaggregationstatus[$itemid] = 'extra';
|
|
}
|
|
}
|
|
|
|
asort($values, SORT_NUMERIC);
|
|
|
|
// let's see we have still enough grades to do any statistics
|
|
if (count($values) == 0) {
|
|
// not enough attempts yet
|
|
$altered[$do] = null;
|
|
unset($todo[$key]);
|
|
$found = true;
|
|
continue;
|
|
}
|
|
|
|
$usedweights = array();
|
|
$adjustedgrade = $grade_category->aggregate_values_and_adjust_bounds($values, $grade_items, $usedweights);
|
|
|
|
// recalculate the rawgrade back to requested range
|
|
$finalgrade = grade_grade::standardise_score($adjustedgrade['grade'],
|
|
0,
|
|
1,
|
|
$adjustedgrade['grademin'],
|
|
$adjustedgrade['grademax']);
|
|
|
|
foreach ($usedweights as $itemid => $weight) {
|
|
if (!isset($alteredaggregationstatus[$itemid])) {
|
|
$alteredaggregationstatus[$itemid] = 'used';
|
|
}
|
|
$alteredaggregationweight[$itemid] = $weight;
|
|
}
|
|
|
|
$finalgrade = $grade_items[$do]->bounded_grade($finalgrade);
|
|
$alteredgrademin[$do] = $adjustedgrade['grademin'];
|
|
$alteredgrademax[$do] = $adjustedgrade['grademax'];
|
|
// We need to muck with the "in-memory" grade_items records so
|
|
// that subsequent calculations will use the adjusted grademin and grademax.
|
|
$grade_items[$do]->grademin = $adjustedgrade['grademin'];
|
|
$grade_items[$do]->grademax = $adjustedgrade['grademax'];
|
|
|
|
$altered[$do] = $finalgrade;
|
|
unset($todo[$key]);
|
|
$found = true;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!$found) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return array('unknown' => array_combine(array_keys($unknown), array_keys($unknown)), // Left for BC in case some gradereport plugins expect it.
|
|
'unknowngrades' => $unknown,
|
|
'altered' => $altered,
|
|
'alteredgrademax' => $alteredgrademax,
|
|
'alteredgrademin' => $alteredgrademin,
|
|
'alteredaggregationstatus' => $alteredaggregationstatus,
|
|
'alteredaggregationweight' => $alteredaggregationweight);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the grade's value is superior or equal to the grade item's gradepass value, false otherwise.
|
|
*
|
|
* @param grade_item $grade_item An optional grade_item of which gradepass value we can use, saves having to load the grade_grade's grade_item
|
|
* @return bool
|
|
*/
|
|
public function is_passed($grade_item = null) {
|
|
if (empty($grade_item)) {
|
|
if (!isset($this->grade_item)) {
|
|
$this->load_grade_item();
|
|
}
|
|
} else {
|
|
$this->grade_item = $grade_item;
|
|
$this->itemid = $grade_item->id;
|
|
}
|
|
|
|
// Return null if finalgrade is null
|
|
if (is_null($this->finalgrade)) {
|
|
return null;
|
|
}
|
|
|
|
// Return null if gradepass == grademin, gradepass is null, or grade item is a scale and gradepass is 0.
|
|
if (is_null($this->grade_item->gradepass)) {
|
|
return null;
|
|
} else if ($this->grade_item->gradepass == $this->grade_item->grademin) {
|
|
return null;
|
|
} else if ($this->grade_item->gradetype == GRADE_TYPE_SCALE && !grade_floats_different($this->grade_item->gradepass, 0.0)) {
|
|
return null;
|
|
}
|
|
|
|
return $this->finalgrade >= $this->grade_item->gradepass;
|
|
}
|
|
|
|
/**
|
|
* Insert the grade_grade instance into the database.
|
|
*
|
|
* @param string $source From where was the object inserted (mod/forum, manual, etc.)
|
|
* @return int The new grade_grade ID if successful, false otherwise
|
|
*/
|
|
public function insert($source=null) {
|
|
// TODO: dategraded hack - do not update times, they are used for submission and grading (MDL-31379)
|
|
//$this->timecreated = $this->timemodified = time();
|
|
return parent::insert($source);
|
|
}
|
|
|
|
/**
|
|
* In addition to update() as defined in grade_object rounds the float numbers using php function,
|
|
* the reason is we need to compare the db value with computed number to skip updates if possible.
|
|
*
|
|
* @param string $source from where was the object inserted (mod/forum, manual, etc.)
|
|
* @return bool success
|
|
*/
|
|
public function update($source=null) {
|
|
$this->rawgrade = grade_floatval($this->rawgrade);
|
|
$this->finalgrade = grade_floatval($this->finalgrade);
|
|
$this->rawgrademin = grade_floatval($this->rawgrademin);
|
|
$this->rawgrademax = grade_floatval($this->rawgrademax);
|
|
return parent::update($source);
|
|
}
|
|
|
|
|
|
/**
|
|
* Handles adding feedback files in the gradebook.
|
|
*
|
|
* @param int|null $historyid
|
|
*/
|
|
protected function add_feedback_files(int $historyid = null) {
|
|
global $CFG;
|
|
|
|
// We only support feedback files for modules atm.
|
|
if ($this->grade_item && $this->grade_item->is_external_item()) {
|
|
$context = $this->get_context();
|
|
$this->copy_feedback_files($context, GRADE_FEEDBACK_FILEAREA, $this->id);
|
|
|
|
if (empty($CFG->disablegradehistory) && $historyid) {
|
|
$this->copy_feedback_files($context, GRADE_HISTORY_FEEDBACK_FILEAREA, $historyid);
|
|
}
|
|
}
|
|
|
|
return $this->id;
|
|
}
|
|
|
|
/**
|
|
* Handles updating feedback files in the gradebook.
|
|
*
|
|
* @param int|null $historyid
|
|
*/
|
|
protected function update_feedback_files(int $historyid = null) {
|
|
global $CFG;
|
|
|
|
// We only support feedback files for modules atm.
|
|
if ($this->grade_item && $this->grade_item->is_external_item()) {
|
|
$context = $this->get_context();
|
|
|
|
$fs = new file_storage();
|
|
$fs->delete_area_files($context->id, GRADE_FILE_COMPONENT, GRADE_FEEDBACK_FILEAREA, $this->id);
|
|
|
|
$this->copy_feedback_files($context, GRADE_FEEDBACK_FILEAREA, $this->id);
|
|
|
|
if (empty($CFG->disablegradehistory) && $historyid) {
|
|
$this->copy_feedback_files($context, GRADE_HISTORY_FEEDBACK_FILEAREA, $historyid);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Handles deleting feedback files in the gradebook.
|
|
*/
|
|
protected function delete_feedback_files() {
|
|
// We only support feedback files for modules atm.
|
|
if ($this->grade_item && $this->grade_item->is_external_item()) {
|
|
$context = $this->get_context();
|
|
|
|
$fs = new file_storage();
|
|
$fs->delete_area_files($context->id, GRADE_FILE_COMPONENT, GRADE_FEEDBACK_FILEAREA, $this->id);
|
|
|
|
// Grade history only gets deleted when we delete the whole grade item.
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Deletes the grade_grade instance from the database.
|
|
*
|
|
* @param string $source The location the deletion occurred (mod/forum, manual, etc.).
|
|
* @return bool Returns true if the deletion was successful, false otherwise.
|
|
*/
|
|
public function delete($source = null) {
|
|
$success = parent::delete($source);
|
|
|
|
// If the grade was deleted successfully trigger a grade_deleted event.
|
|
if ($success) {
|
|
$this->load_grade_item();
|
|
\core\event\grade_deleted::create_from_grade($this)->trigger();
|
|
}
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* Used to notify the completion system (if necessary) that a user's grade
|
|
* has changed, and clear up a possible score cache.
|
|
*
|
|
* @param bool $deleted True if grade was actually deleted
|
|
*/
|
|
protected function notify_changed($deleted) {
|
|
global $CFG;
|
|
|
|
// Condition code may cache the grades for conditional availability of
|
|
// modules or sections. (This code should use a hook for communication
|
|
// with plugin, but hooks are not implemented at time of writing.)
|
|
if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
|
|
\availability_grade\callbacks::grade_changed($this->userid);
|
|
}
|
|
|
|
require_once($CFG->libdir.'/completionlib.php');
|
|
|
|
// Bail out immediately if completion is not enabled for site (saves loading
|
|
// grade item & requiring the restore stuff).
|
|
if (!completion_info::is_enabled_for_site()) {
|
|
return;
|
|
}
|
|
|
|
// Ignore during restore, as completion data will be updated anyway and
|
|
// doing it now will result in incorrect dates (it will say they got the
|
|
// grade completion now, instead of the correct time).
|
|
if (class_exists('restore_controller', false) && restore_controller::is_executing()) {
|
|
return;
|
|
}
|
|
|
|
// Load information about grade item
|
|
$this->load_grade_item();
|
|
|
|
// Only course-modules have completion data
|
|
if ($this->grade_item->itemtype!='mod') {
|
|
return;
|
|
}
|
|
|
|
// Use $COURSE if available otherwise get it via item fields
|
|
$course = get_course($this->grade_item->courseid, false);
|
|
|
|
// Bail out if completion is not enabled for course
|
|
$completion = new completion_info($course);
|
|
if (!$completion->is_enabled()) {
|
|
return;
|
|
}
|
|
|
|
// Get course-module
|
|
$cm = get_coursemodule_from_instance($this->grade_item->itemmodule,
|
|
$this->grade_item->iteminstance, $this->grade_item->courseid);
|
|
// If the course-module doesn't exist, display a warning...
|
|
if (!$cm) {
|
|
// ...unless the grade is being deleted in which case it's likely
|
|
// that the course-module was just deleted too, so that's okay.
|
|
if (!$deleted) {
|
|
debugging("Couldn't find course-module for module '" .
|
|
$this->grade_item->itemmodule . "', instance '" .
|
|
$this->grade_item->iteminstance . "', course '" .
|
|
$this->grade_item->courseid . "'");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Pass information on to completion system
|
|
$completion->inform_grade_changed($cm, $this->grade_item, $this, $deleted);
|
|
}
|
|
|
|
/**
|
|
* Get some useful information about how this grade_grade is reflected in the aggregation
|
|
* for the grade_category. For example this could be an extra credit item, and it could be
|
|
* dropped because it's in the X lowest or highest.
|
|
*
|
|
* @return array(status, weight) - A keyword and a numerical weight that represents how this grade was included in the aggregation.
|
|
*/
|
|
function get_aggregation_hint() {
|
|
return array('status' => $this->get_aggregationstatus(),
|
|
'weight' => $this->get_aggregationweight());
|
|
}
|
|
|
|
/**
|
|
* Handles copying feedback files to a specified gradebook file area.
|
|
*
|
|
* @param context $context
|
|
* @param string $filearea
|
|
* @param int $itemid
|
|
*/
|
|
private function copy_feedback_files(context $context, string $filearea, int $itemid) {
|
|
if ($this->feedbackfiles) {
|
|
$filestocopycontextid = $this->feedbackfiles['contextid'];
|
|
$filestocopycomponent = $this->feedbackfiles['component'];
|
|
$filestocopyfilearea = $this->feedbackfiles['filearea'];
|
|
$filestocopyitemid = $this->feedbackfiles['itemid'];
|
|
|
|
$fs = new file_storage();
|
|
if ($filestocopy = $fs->get_area_files($filestocopycontextid, $filestocopycomponent, $filestocopyfilearea,
|
|
$filestocopyitemid)) {
|
|
foreach ($filestocopy as $filetocopy) {
|
|
$destination = [
|
|
'contextid' => $context->id,
|
|
'component' => GRADE_FILE_COMPONENT,
|
|
'filearea' => $filearea,
|
|
'itemid' => $itemid
|
|
];
|
|
$fs->create_file_from_storedfile($destination, $filetocopy);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine the correct context for this grade_grade.
|
|
*
|
|
* @return context
|
|
*/
|
|
public function get_context() {
|
|
$this->load_grade_item();
|
|
return $this->grade_item->get_context();
|
|
}
|
|
}
|
|
|