/** * Given a list of all assessments of a single submission, updates the grading grades in database * * @param array $assessments of stdclass (->assessmentid ->assessmentweight ->reviewerid ->gradinggrade ->submissionid ->dimensionid ->grade) * @param array $diminfo of stdclass (->id ->weight ->max ->min) * @param stdClass grading evaluation settings * @return void */ protected function process_assessments(array $assessments, array $diminfo, stdclass $settings) { global $DB; if (empty($assessments)) { return; } // reindex the passed flat structure to be indexed by assessmentid $assessments = $this->prepare_data_from_recordset($assessments); // normalize the dimension grades to the interval 0 - 100 $assessments = $this->normalize_grades($assessments, $diminfo); // get a hypothetical average assessment $average = $this->average_assessment($assessments); // calculate variance of dimension grades $variances = $this->weighted_variance($assessments); foreach ($variances as $dimid => $variance) { $diminfo[$dimid]->variance = $variance; } // for every assessment, calculate its distance from the average one $distances = array(); foreach ($assessments as $asid => $assessment) { $distances[$asid] = $this->assessments_distance($assessment, $average, $diminfo, $settings); } // identify the best assessments - that is those with the shortest distance from the best assessment $bestids = array_keys($distances, min($distances)); // for every assessment, calculate its distance from the nearest best assessment $distances = array(); foreach ($bestids as $bestid) { $best = $assessments[$bestid]; foreach ($assessments as $asid => $assessment) { $d = $this->assessments_distance($assessment, $best, $diminfo, $settings); if (!is_null($d) and (!isset($distances[$asid]) or $d < $distances[$asid])) { $distances[$asid] = $d; } } } // calculate the grading grade foreach ($distances as $asid => $distance) { $gradinggrade = (100 - $distance); if ($gradinggrade < 0) { $gradinggrade = 0; } if ($gradinggrade > 100) { $gradinggrade = 100; } $grades[$asid] = grade_floatval($gradinggrade); } // if the new grading grade differs from the one stored in database, update it // we do not use set_field() here because we want to pass $bulk param foreach ($grades as $assessmentid => $grade) { if (grade_floats_different($grade, $assessments[$assessmentid]->gradinggrade)) { // the value has changed $record = new stdclass(); $record->id = $assessmentid; $record->gradinggrade = grade_floatval($grade); // do not set timemodified here, it contains the timestamp of when the form was // saved by the peer reviewer, not when it was aggregated $DB->update_record('workshop_assessments', $record, true); // bulk operations expected } } // done. easy, heh? ;-) }
/** * Submit new or update grade; update/create grade_item definition. Grade must have userid specified, * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded'. * Missing property or key means does not change the existing value. * * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax', * 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones. * * Manual, course or category items can not be updated by this function. * * @category grade * @param string $source Source of the grade such as 'mod/assignment' * @param int $courseid ID of course * @param string $itemtype Type of grade item. For example, mod or block * @param string $itemmodule More specific then $itemtype. For example, assignment or forum. May be NULL for some item types * @param int $iteminstance Instance ID of graded item * @param int $itemnumber Most probably 0. Modules can use other numbers when having more than one grade for each user * @param mixed $grades Grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only * @param mixed $itemdetails Object or array describing the grading item, NULL if no change * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED */ function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades = NULL, $itemdetails = NULL) { global $USER, $CFG, $DB; // only following grade_item properties can be changed in this function $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden'); // list of 10,5 numeric fields $floats = array('grademin', 'grademax', 'multfactor', 'plusfactor'); // grade item identification $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber'); if (is_null($courseid) or is_null($itemtype)) { debugging('Missing courseid or itemtype'); return GRADE_UPDATE_FAILED; } if (!($grade_items = grade_item::fetch_all($params))) { // create a new one $grade_item = false; } else { if (count($grade_items) == 1) { $grade_item = reset($grade_items); unset($grade_items); //release memory } else { debugging('Found more than one grade item'); return GRADE_UPDATE_MULTIPLE; } } if (!empty($itemdetails['deleted'])) { if ($grade_item) { if ($grade_item->delete($source)) { return GRADE_UPDATE_OK; } else { return GRADE_UPDATE_FAILED; } } return GRADE_UPDATE_OK; } /// Create or update the grade_item if needed if (!$grade_item) { if ($itemdetails) { $itemdetails = (array) $itemdetails; // grademin and grademax ignored when scale specified if (array_key_exists('scaleid', $itemdetails)) { if ($itemdetails['scaleid']) { unset($itemdetails['grademin']); unset($itemdetails['grademax']); } } foreach ($itemdetails as $k => $v) { if (!in_array($k, $allowed)) { // ignore it continue; } if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) { // no grade item needed! return GRADE_UPDATE_OK; } $params[$k] = $v; } } $grade_item = new grade_item($params); $grade_item->insert(); } else { if ($grade_item->is_locked()) { // no notice() here, test returned value instead! return GRADE_UPDATE_ITEM_LOCKED; } if ($itemdetails) { $itemdetails = (array) $itemdetails; $update = false; foreach ($itemdetails as $k => $v) { if (!in_array($k, $allowed)) { // ignore it continue; } if (in_array($k, $floats)) { if (grade_floats_different($grade_item->{$k}, $v)) { $grade_item->{$k} = $v; $update = true; } } else { if ($grade_item->{$k} != $v) { $grade_item->{$k} = $v; $update = true; } } } if ($update) { $grade_item->update(); } } } /// reset grades if requested if (!empty($itemdetails['reset'])) { $grade_item->delete_all_grades('reset'); return GRADE_UPDATE_OK; } /// Some extra checks // do we use grading? if ($grade_item->gradetype == GRADE_TYPE_NONE) { return GRADE_UPDATE_OK; } // no grade submitted if (empty($grades)) { return GRADE_UPDATE_OK; } /// Finally start processing of grades if (is_object($grades)) { $grades = array($grades->userid => $grades); } else { if (array_key_exists('userid', $grades)) { $grades = array($grades['userid'] => $grades); } } /// normalize and verify grade array foreach ($grades as $k => $g) { if (!is_array($g)) { $g = (array) $g; $grades[$k] = $g; } if (empty($g['userid']) or $k != $g['userid']) { debugging('Incorrect grade array index, must be user id! Grade ignored.'); unset($grades[$k]); } } if (empty($grades)) { return GRADE_UPDATE_FAILED; } $count = count($grades); if ($count > 0 and $count < 200) { list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED, $start = 'uid'); $params['gid'] = $grade_item->id; $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid {$uids}"; } else { $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid"; $params = array('gid' => $grade_item->id); } $rs = $DB->get_recordset_sql($sql, $params); $failed = false; while (count($grades) > 0) { $grade_grade = null; $grade = null; foreach ($rs as $gd) { $userid = $gd->userid; if (!isset($grades[$userid])) { // this grade not requested, continue continue; } // existing grade requested $grade = $grades[$userid]; $grade_grade = new grade_grade($gd, false); unset($grades[$userid]); break; } if (is_null($grade_grade)) { if (count($grades) == 0) { // no more grades to process break; } $grade = reset($grades); $userid = $grade['userid']; $grade_grade = new grade_grade(array('itemid' => $grade_item->id, 'userid' => $userid), false); $grade_grade->load_optional_fields(); // add feedback and info too unset($grades[$userid]); } $rawgrade = false; $feedback = false; $feedbackformat = FORMAT_MOODLE; $usermodified = $USER->id; $datesubmitted = null; $dategraded = null; if (array_key_exists('rawgrade', $grade)) { $rawgrade = $grade['rawgrade']; } if (array_key_exists('feedback', $grade)) { $feedback = $grade['feedback']; } if (array_key_exists('feedbackformat', $grade)) { $feedbackformat = $grade['feedbackformat']; } if (array_key_exists('usermodified', $grade)) { $usermodified = $grade['usermodified']; } if (array_key_exists('datesubmitted', $grade)) { $datesubmitted = $grade['datesubmitted']; } if (array_key_exists('dategraded', $grade)) { $dategraded = $grade['dategraded']; } // update or insert the grade if (!$grade_item->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified, $dategraded, $datesubmitted, $grade_grade)) { $failed = true; } } if ($rs) { $rs->close(); } if (!$failed) { return GRADE_UPDATE_OK; } else { return GRADE_UPDATE_FAILED; } }
/** * 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; }
/** * internal function - does the final grade calculation */ function use_formula($userid, $params, $useditems, $oldgrade) { if (empty($userid)) { return true; } // add missing final grade values // not graded (null) is counted as 0 - the spreadsheet way foreach ($useditems as $gi) { if (!array_key_exists('gi' . $gi, $params)) { $params['gi' . $gi] = 0; } else { $params['gi' . $gi] = (double) $params['gi' . $gi]; } } // can not use own final grade during calculation unset($params['gi' . $this->id]); // insert final grade - will be needed later anyway if ($oldgrade) { $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); // fetching from db is not needed $grade->grade_item =& $this; } else { $grade = new grade_grade(array('itemid' => $this->id, 'userid' => $userid), false); $grade->grade_item =& $this; $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return true; } // do the calculation $this->formula->set_params($params); $result = $this->formula->evaluate(); if ($result === false) { $grade->finalgrade = null; } else { // normalize $result = bounded_number($this->grademin, $result, $this->grademax); if ($this->gradetype == GRADE_TYPE_SCALE) { $result = round($result + 1.0E-5); // round scales upwards } $grade->finalgrade = $result; } // update in db if changed if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->update('compute'); } if ($result !== false) { //lock grade if needed } if ($result === false) { return false; } else { return true; } }
/** * internal function for category grades aggregation * * @param int $userid * @param array $items * @param array $grade_values * @param object $oldgrade * @param bool $excluded * @return boolean (just plain return;) */ function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded, $grade_max, $grade_min) { global $CFG; if (empty($userid)) { //ignore first call return; } if ($oldgrade) { $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade_local($oldgrade, false); $grade->grade_item =& $this->grade_item; } else { // insert final grade - it will be needed later anyway $grade = new grade_grade_local(array('itemid' => $this->grade_item->id, 'userid' => $userid), false); $grade->grade_item =& $this->grade_item; $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return; } // can not use own final category grade in calculation unset($grade_values[$this->grade_item->id]); /// sum is a special aggregation types - it adjusts the min max, does not use relative values // HACK: Bob Puffer 12/8/09 to allow for correct summing of point totals for all aggregation methods if ($this->aggregation == GRADE_AGGREGATE_SUM) { $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded); return; } // CONSIDER REINSTATING THIS IF IT DOESN'T WORK OUT TO REMOVE IT // sum all aggregation methods // $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded); // END OF HACK // if no grades calculation possible or grading not allowed clear final grade if (empty($grade_values) or empty($items) or $this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE) { $grade->finalgrade = null; if (!is_null($oldfinalgrade)) { $grade->update('aggregation'); } return; } /// normalize the grades first - all will have value 0...1 // ungraded items are not used in aggregation foreach ($grade_values as $itemid => $v) { if (is_null($v)) { // null means no grade unset($grade_values[$itemid]); continue; } else { if (in_array($itemid, $excluded)) { unset($grade_values[$itemid]); continue; } } // $grade_values[$itemid] = grade_grade::standardise_score($v, $items[$itemid]->grademin, $items[$itemid]->grademax, 0, 1); $items[$itemid]->grademax = $grade_max[$itemid]; $grade_values[$itemid] = grade_grade::standardise_score($v, $grade_min[$itemid], $grade_max[$itemid], 0, 1); } // use min grade if grade missing for these types if (!$this->aggregateonlygraded) { foreach ($items as $itemid => $value) { if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) { $grade_values[$itemid] = 0; } } } // limit and sort $this->apply_limit_rules($grade_values, $items); asort($grade_values, SORT_NUMERIC); // HACK 10/29/09 Bob Puffer to allow accurate computation of category maxgrade // has to be done after any dropped grades are dropped $cat_max = 0; // END OF HACK foreach ($grade_values as $itemid => $v) { if ($items[$itemid]->aggregationcoef == 1 and $this->aggregation != GRADE_AGGREGATE_WEIGHTED_MEAN) { } else { if ($items[$itemid]->itemtype == 'category') { // $gradegradesrec = new grade_grade_local(array('itemid'=>$itemid, 'userid'=>$userid), true); // if (isset($gradegradesrec->itemid->itemtype) AND $gradegradesrec->itemid->itemtype == 'category') { // $cat_max += $gradegradesrec->rawgrademax; $cat_max += $grade_max[$itemid]; } else { // $cat_max += $items[$itemid]->grademax; // $cat_max += $gradegradesrec->itemid->grademax; $cat_max += $grade_max[$itemid]; // } } } } // END OF HACK // let's see we have still enough grades to do any statistics if (count($grade_values) == 0) { // not enough attempts yet $grade->finalgrade = null; if (!is_null($oldfinalgrade)) { $grade->update('aggregation'); } return; } // do the maths // HACK: Bob Puffer 10/29/09 to allow proper totalling of points for the category // $agg_grade = $this->aggregate_values($grade_values, $items); // recalculate the grade back to requested range // $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax); $this->grade_item->grademax = $cat_max; $oldmaxgrade = $grade->rawgrademax; $grade->rawgrademax = $cat_max; // HACK we don't want to call the aggregate_values function if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) { $weightsum = 0; $sum = null; foreach ($grade_values as $itemid => $grade_value) { $weight = $grade_max[$itemid] - $grade_min[$itemid]; // $weight = $items[$itemid]->grademax - $items[$itemid]->grademin; if ($weight <= 0) { continue; } $sum += $weight * $grade_value; } if ($weightsum == 0) { $finalgrade = $sum; // only extra credits } else { $finalgrade = $sum / $weightsum; } } else { $agg_grade = $this->aggregate_values($grade_values, $items); // recalculate the grade back to requested range $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax); } // END OF HACK $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade); // update in db if changed // HACK to update category maxes in the db if they change // if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { if (grade_floats_different($grade->finalgrade, $oldfinalgrade) or grade_floats_different($grade->rawgrademax, $oldmaxgrade)) { $grade->update('aggregation'); } return; }
/** * internal function for category grades summing * * @param object $grade * @param int $userid * @param float $oldfinalgrade * @param array $items * @param array $grade_values * @param bool $excluded * @return boolean (just plain return;) */ function sum_grades(&$grade, $oldfinalgrade, $items, $grade_values, $excluded) { // ungraded and exluded items are not used in aggregation foreach ($grade_values as $itemid => $v) { if (is_null($v)) { unset($grade_values[$itemid]); } else { if (in_array($itemid, $excluded)) { unset($grade_values[$itemid]); } } } // use 0 if grade missing, droplow used and aggregating all items if (!$this->aggregateonlygraded and !empty($this->droplow)) { foreach ($items as $itemid => $value) { if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) { $grade_values[$itemid] = 0; } } } $max = 0; //find max grade foreach ($items as $item) { if ($item->aggregationcoef > 0) { // extra credit from this activity - does not affect total continue; } if ($item->gradetype == GRADE_TYPE_VALUE) { $max += $item->grademax; } else { if ($item->gradetype == GRADE_TYPE_SCALE) { $max += $item->grademax - 1; // scales min is 1 } } } if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) { $this->grade_item->grademax = $max; $this->grade_item->grademin = 0; $this->grade_item->gradetype = GRADE_TYPE_VALUE; $this->grade_item->update('aggregation'); } $this->apply_limit_rules($grade_values); $sum = array_sum($grade_values); $grade->finalgrade = bounded_number($this->grade_item->grademin, $sum, $this->grade_item->grademax); // update in db if changed if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->update('aggregation'); } return; }
/** * Internal function that does the final grade calculation * * @param int $userid The user ID * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database * @return bool False if an error occurred */ public function use_formula($userid, $params, $useditems, $oldgrade) { if (empty($userid)) { return true; } // add missing final grade values // not graded (null) is counted as 0 - the spreadsheet way $allinputsnull = true; foreach ($useditems as $gi) { if (!array_key_exists('gi' . $gi, $params) || is_null($params['gi' . $gi])) { $params['gi' . $gi] = 0; } else { $params['gi' . $gi] = (double) $params['gi' . $gi]; if ($gi != $this->id) { $allinputsnull = false; } } } // can not use own final grade during calculation unset($params['gi' . $this->id]); // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they // wish to update the grades. $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid); $rawminandmaxchanged = false; // insert final grade - will be needed later anyway if ($oldgrade) { // Only run through this code if the gradebook isn't frozen. if ($gradebookcalculationsfreeze && (int) $gradebookcalculationsfreeze <= 20150627) { // Do nothing. } else { // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the // grade_item grade maximum and minimum respectively. if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) { $rawminandmaxchanged = true; $oldgrade->rawgrademax = $this->grademax; $oldgrade->rawgrademin = $this->grademin; } } $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); // fetching from db is not needed $grade->grade_item =& $this; } else { $grade = new grade_grade(array('itemid' => $this->id, 'userid' => $userid), false); $grade->grade_item =& $this; $rawminandmaxchanged = false; if ($gradebookcalculationsfreeze && (int) $gradebookcalculationsfreeze <= 20150627) { // Do nothing. } else { // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the // grade_item grade maximum and minimum respectively. $rawminandmaxchanged = true; $grade->rawgrademax = $this->grademax; $grade->rawgrademin = $this->grademin; } $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return true; } if ($allinputsnull) { $grade->finalgrade = null; $result = true; } else { // do the calculation $this->formula->set_params($params); $result = $this->formula->evaluate(); if ($result === false) { $grade->finalgrade = null; } else { // normalize $grade->finalgrade = $this->bounded_grade($result); } } // Only run through this code if the gradebook isn't frozen. if ($gradebookcalculationsfreeze && (int) $gradebookcalculationsfreeze <= 20150627) { // Update in db if changed. if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->timemodified = time(); $success = $grade->update('compute'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } } else { // Update in db if changed. if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) { $grade->timemodified = time(); $success = $grade->update('compute'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } } if ($result !== false) { //lock grade if needed } if ($result === false) { return false; } else { return true; } }
/** * Given an array of all assessments done by a single reviewer, calculates the final grading grade * * This calculates the simple mean of the passed grading grades. If, however, the grading grade * was overridden by a teacher, the gradinggradeover value is returned and the rest of grades are ignored. * * @param array $assessments of stdclass(->reviewerid ->gradinggrade ->gradinggradeover ->aggregationid ->aggregatedgrade) * @return void */ protected function aggregate_grading_grades_process(array $assessments) { global $DB; $reviewerid = null; // the id of the reviewer being processed $current = null; // the gradinggrade currently saved in database $finalgrade = null; // the new grade to be calculated $agid = null; // aggregation id $sumgrades = 0; $count = 0; foreach ($assessments as $assessment) { if (is_null($reviewerid)) { // the id is the same in all records, fetch it during the first loop cycle $reviewerid = $assessment->reviewerid; } if (is_null($agid)) { // the id is the same in all records, fetch it during the first loop cycle $agid = $assessment->aggregationid; } if (is_null($current)) { // the currently saved grade is the same in all records, fetch it during the first loop cycle $current = $assessment->aggregatedgrade; } if (!is_null($assessment->gradinggradeover)) { // the grading grade for this assessment is overridden by a teacher $sumgrades += $assessment->gradinggradeover; $count++; } else { if (!is_null($assessment->gradinggrade)) { $sumgrades += $assessment->gradinggrade; $count++; } } } if ($count > 0) { $finalgrade = grade_floatval($sumgrades / $count); } // check if the new final grade differs from the one stored in the database if (grade_floats_different($finalgrade, $current)) { // we need to save new calculation into the database if (is_null($agid)) { // no aggregation record yet $record = new stdclass(); $record->workshopid = $this->id; $record->userid = $reviewerid; $record->gradinggrade = $finalgrade; $record->timegraded = time(); $DB->insert_record('workshop_aggregations', $record); } else { $record = new stdclass(); $record->id = $agid; $record->gradinggrade = $finalgrade; $record->timegraded = time(); $DB->update_record('workshop_aggregations', $record); } } }
/** * Internal function for grade category grade aggregation * * @param int $userid The User ID * @param array $items Grade items * @param array $grade_values Array of grade values * @param object $oldgrade Old grade * @param array $excluded Excluded * @param array $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid) * @param array $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid) */ private function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded, $grademinoverrides, $grademaxoverrides) { global $CFG, $DB; // Remember these so we can set flags on them to describe how they were used in the aggregation. $novalue = array(); $dropped = array(); $extracredit = array(); $usedweights = array(); if (empty($userid)) { //ignore first call return; } if ($oldgrade) { $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); $grade->grade_item =& $this->grade_item; } else { // insert final grade - it will be needed later anyway $grade = new grade_grade(array('itemid' => $this->grade_item->id, 'userid' => $userid), false); $grade->grade_item =& $this->grade_item; $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return; } // can not use own final category grade in calculation unset($grade_values[$this->grade_item->id]); // Make sure a grade_grade exists for every grade_item. // We need to do this so we can set the aggregationstatus // with a set_field call instead of checking if each one exists and creating/updating. if (!empty($items)) { list($ggsql, $params) = $DB->get_in_or_equal(array_keys($items), SQL_PARAMS_NAMED, 'g'); $params['userid'] = $userid; $sql = "SELECT itemid\n FROM {grade_grades}\n WHERE itemid {$ggsql} AND userid = :userid"; $existingitems = $DB->get_records_sql($sql, $params); $notexisting = array_diff(array_keys($items), array_keys($existingitems)); foreach ($notexisting as $itemid) { $gradeitem = $items[$itemid]; $gradegrade = new grade_grade(array('itemid' => $itemid, 'userid' => $userid, 'rawgrademin' => $gradeitem->grademin, 'rawgrademax' => $gradeitem->grademax), false); $gradegrade->grade_item = $gradeitem; $gradegrade->insert('system'); } } // if no grades calculation possible or grading not allowed clear final grade if (empty($grade_values) or empty($items) or $this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE) { $grade->finalgrade = null; if (!is_null($oldfinalgrade)) { $success = $grade->update('aggregation'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } $dropped = $grade_values; $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit); return; } // Normalize the grades first - all will have value 0...1 // ungraded items are not used in aggregation. foreach ($grade_values as $itemid => $v) { if (is_null($v)) { // If null, it means no grade. if ($this->aggregateonlygraded) { unset($grade_values[$itemid]); // Mark this item as "excluded empty" because it has no grade. $novalue[$itemid] = 0; continue; } } if (in_array($itemid, $excluded)) { unset($grade_values[$itemid]); $dropped[$itemid] = 0; continue; } // Check for user specific grade min/max overrides. $usergrademin = $items[$itemid]->grademin; $usergrademax = $items[$itemid]->grademax; if (isset($grademinoverrides[$itemid])) { $usergrademin = $grademinoverrides[$itemid]; } if (isset($grademaxoverrides[$itemid])) { $usergrademax = $grademaxoverrides[$itemid]; } if ($this->aggregation == GRADE_AGGREGATE_SUM) { // Assume that the grademin is 0 when standardising the score, to preserve negative grades. $grade_values[$itemid] = grade_grade::standardise_score($v, 0, $usergrademax, 0, 1); } else { $grade_values[$itemid] = grade_grade::standardise_score($v, $usergrademin, $usergrademax, 0, 1); } } // For items with no value, and not excluded - either set their grade to 0 or exclude them. foreach ($items as $itemid => $value) { if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) { if (!$this->aggregateonlygraded) { $grade_values[$itemid] = 0; } else { // We are specifically marking these items as "excluded empty". $novalue[$itemid] = 0; } } } // limit and sort $allvalues = $grade_values; if ($this->can_apply_limit_rules()) { $this->apply_limit_rules($grade_values, $items); } $moredropped = array_diff($allvalues, $grade_values); foreach ($moredropped as $drop => $unused) { $dropped[$drop] = 0; } foreach ($grade_values as $itemid => $val) { if (self::is_extracredit_used() && $items[$itemid]->aggregationcoef > 0) { $extracredit[$itemid] = 0; } } asort($grade_values, SORT_NUMERIC); // let's see we have still enough grades to do any statistics if (count($grade_values) == 0) { // not enough attempts yet $grade->finalgrade = null; if (!is_null($oldfinalgrade)) { $success = $grade->update('aggregation'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit); return; } // do the maths $result = $this->aggregate_values_and_adjust_bounds($grade_values, $items, $usedweights, $grademinoverrides, $grademaxoverrides); $agg_grade = $result['grade']; // Set the actual grademin and max to bind the grade properly. $this->grade_item->grademin = $result['grademin']; $this->grade_item->grademax = $result['grademax']; if ($this->aggregation == GRADE_AGGREGATE_SUM) { // The natural aggregation always displays the range as coming from 0 for categories. // However, when we bind the grade we allow for negative values. $result['grademin'] = 0; } // Recalculate the grade back to requested range. $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $result['grademin'], $result['grademax']); $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade); $oldrawgrademin = $grade->rawgrademin; $oldrawgrademax = $grade->rawgrademax; $grade->rawgrademin = $result['grademin']; $grade->rawgrademax = $result['grademax']; // Update in db if changed. if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || grade_floats_different($grade->rawgrademax, $oldrawgrademax) || grade_floats_different($grade->rawgrademin, $oldrawgrademin)) { $success = $grade->update('aggregation'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit); return; }
/** * Called by HTML_QuickForm whenever form event is made on this element. * * @param string $event Name of event * @param mixed $arg event arguments * @param moodleform $caller calling object * @return mixed */ public function onQuickFormEvent($event, $arg, &$caller) { switch ($event) { case 'createElement': // The first argument is the name. $name = $arg[0]; // Set disable actions. $caller->disabledIf($name . '[modgrade_scale]', $name . '[modgrade_type]', 'neq', 'scale'); $caller->disabledIf($name . '[modgrade_point]', $name . '[modgrade_type]', 'neq', 'point'); $caller->disabledIf($name . '[modgrade_rescalegrades]', $name . '[modgrade_type]', 'neq', 'point'); // Set validation rules for the sub-elements belonging to this element. // A handy note: the parent scope of a closure is the function in which the closure was declared. // Because of this using $this is safe despite the closures being called statically. // A nasty magic hack! $checkgradetypechange = function ($val) { // Nothing is affected by changes to the grade type if there are no grades yet. if (!$this->hasgrades) { return true; } // Check if we are changing the grade type when grades are present. if (isset($val['modgrade_type']) && $val['modgrade_type'] !== $this->currentgradetype) { return false; } return true; }; $checkscalechange = function ($val) { // Nothing is affected by changes to the scale if there are no grades yet. if (!$this->hasgrades) { return true; } // Check if we are changing the scale type when grades are present. if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'scale') { if (isset($val['modgrade_scale']) && $val['modgrade_scale'] !== $this->currentscaleid) { return false; } } return true; }; $checkmaxgradechange = function ($val) { // Nothing is affected by changes to the max grade if there are no grades yet. if (!$this->hasgrades) { return true; } // If we are not using ratings we can change the max grade. if (!$this->useratings) { return true; } // Check if we are changing the max grade if we are using ratings and there is a grade. if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'point') { if (isset($val['modgrade_point']) && grade_floats_different($this->currentgrade, $val['modgrade_point'])) { return false; } } return true; }; $checkmaxgrade = function ($val) { // Closure to validate a max points value. See the note above about scope if this confuses you. if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'point') { if (!isset($val['modgrade_point'])) { return false; } return $this->validate_point($val['modgrade_point']); } return true; }; $checkvalidscale = function ($val) { // Closure to validate a scale value. See the note above about scope if this confuses you. if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'scale') { if (!isset($val['modgrade_scale'])) { return false; } return $this->validate_scale($val['modgrade_scale']); } return true; }; $checkrescale = function ($val) { // Nothing is affected by changes to grademax if there are no grades yet. if (!$this->isupdate || !$this->hasgrades || !$this->canrescale) { return true; } // Closure to validate a scale value. See the note above about scope if this confuses you. if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'point') { // Work out if the value was actually changed in the form. if (grade_floats_different($this->currentgrade, $val['modgrade_point'])) { if (empty($val['modgrade_rescalegrades'])) { // This was an "edit", the grademax was changed and the process existing setting was not set. return false; } } } return true; }; $cantchangegradetype = get_string('modgradecantchangegradetype', 'grades'); $cantchangemaxgrade = get_string('modgradecantchangeratingmaxgrade', 'grades'); $maxgradeexceeded = get_string('modgradeerrorbadpoint', 'grades', get_config('core', 'gradepointmax')); $invalidscale = get_string('modgradeerrorbadscale', 'grades'); $cantchangescale = get_string('modgradecantchangescale', 'grades'); $mustchooserescale = get_string('mustchooserescaleyesorno', 'grades'); // When creating the rules the sixth arg is $force, we set it to true because otherwise the form // will attempt to validate the existence of the element, we don't want this because the element // is being created right now and doesn't actually exist as a registered element yet. $caller->addRule($name, $cantchangegradetype, 'callback', $checkgradetypechange, 'server', false, true); $caller->addRule($name, $cantchangemaxgrade, 'callback', $checkmaxgradechange, 'server', false, true); $caller->addRule($name, $maxgradeexceeded, 'callback', $checkmaxgrade, 'server', false, true); $caller->addRule($name, $invalidscale, 'callback', $checkvalidscale, 'server', false, true); $caller->addRule($name, $cantchangescale, 'callback', $checkscalechange, 'server', false, true); $caller->addRule($name, $mustchooserescale, 'callback', $checkrescale, 'server', false, true); break; case 'updateValue': // As this is a group element with no value of its own we are only interested in situations where the // default value or a constant value are being provided to the actual element. // In this case we expect an int that is going to translate to a scale if negative, or to max points // if positive. // Set the maximum points field to disabled if the rescale option has not been chosen and there are grades. $caller->disabledIf($this->getName() . '[modgrade_point]', $this->getName() . '[modgrade_rescalegrades]', 'eq', ''); // A constant value should be given as an int. // The default value should be an int and should really be $CFG->gradepointdefault. $value = $this->_findValue($caller->_constantValues); if (null === $value) { if ($caller->isSubmitted()) { break; } $value = $this->_findValue($caller->_defaultValues); } if (!is_null($value) && !is_scalar($value)) { // Something unexpected (likely an array of subelement values) has been given - this will be dealt // with somewhere else - where exactly... likely the subelements. debugging('An invalid value (type ' . gettype($value) . ') has arrived at ' . __METHOD__, DEBUG_DEVELOPER); break; } // Set element state for existing data. // This is really a pretty hacky thing to do, when data is being set the group element is called // with the data first and the subelements called afterwards. // This means that the subelements data (inc const and default values) can be overridden by form code. // So - when we call this code really we can't be sure that will be the end value for the element. if (!empty($this->_elements)) { if (!empty($value)) { if ($value < 0) { $this->gradetypeformelement->setValue('scale'); $this->scaleformelement->setValue($value * -1); } else { if ($value > 0) { $this->gradetypeformelement->setValue('point'); $this->maxgradeformelement->setValue($value); } } } else { $this->gradetypeformelement->setValue('none'); $this->maxgradeformelement->setValue(''); } } break; } // Always let the parent do its thing! return parent::onQuickFormEvent($event, $arg, $caller); }
/** * Internal function that does the final grade calculation * * @param int $userid The user ID * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database * @return bool False if an error occurred */ public function use_formula($userid, $params, $useditems, $oldgrade) { if (empty($userid)) { return true; } // add missing final grade values // not graded (null) is counted as 0 - the spreadsheet way $allinputsnull = true; foreach ($useditems as $gi) { if (!array_key_exists('gi' . $gi, $params) || is_null($params['gi' . $gi])) { $params['gi' . $gi] = 0; } else { $params['gi' . $gi] = (double) $params['gi' . $gi]; if ($gi != $this->id) { $allinputsnull = false; } } } // can not use own final grade during calculation unset($params['gi' . $this->id]); // insert final grade - will be needed later anyway if ($oldgrade) { $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); // fetching from db is not needed $grade->grade_item =& $this; } else { $grade = new grade_grade(array('itemid' => $this->id, 'userid' => $userid), false); $grade->grade_item =& $this; $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return true; } if ($allinputsnull) { $grade->finalgrade = null; $result = true; } else { // do the calculation $this->formula->set_params($params); $result = $this->formula->evaluate(); if ($result === false) { $grade->finalgrade = null; } else { // normalize $grade->finalgrade = $this->bounded_grade($result); } } // update in db if changed if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->timemodified = time(); $grade->update('compute'); } if ($result !== false) { //lock grade if needed } if ($result === false) { return false; } else { return true; } }
function validation($data, $files) { global $COURSE; $gradeitem = false; if ($data['id']) { $gradecategory = grade_category::fetch(array('id' => $data['id'])); $gradeitem = $gradecategory->load_grade_item(); } $errors = parent::validation($data, $files); if (array_key_exists('grade_item_gradetype', $data) and $data['grade_item_gradetype'] == GRADE_TYPE_SCALE) { if (empty($data['grade_item_scaleid'])) { $errors['grade_item_scaleid'] = get_string('missingscale', 'grades'); } } if (array_key_exists('grade_item_grademin', $data) and array_key_exists('grade_item_grademax', $data)) { if (($data['grade_item_grademax'] != 0 or $data['grade_item_grademin'] != 0) and ($data['grade_item_grademax'] == $data['grade_item_grademin'] or $data['grade_item_grademax'] < $data['grade_item_grademin'])) { $errors['grade_item_grademin'] = get_string('incorrectminmax', 'grades'); $errors['grade_item_grademax'] = get_string('incorrectminmax', 'grades'); } } if ($data['id'] && $gradeitem->has_overridden_grades()) { if ($gradeitem->gradetype == GRADE_TYPE_VALUE) { if (grade_floats_different($data['grade_item_grademin'], $gradeitem->grademin) || grade_floats_different($data['grade_item_grademax'], $gradeitem->grademax)) { if (empty($data['grade_item_rescalegrades'])) { $errors['grade_item_rescalegrades'] = get_string('mustchooserescaleyesorno', 'grades'); } } } } return $errors; }
function validation($data, $files) { global $COURSE; $grade_item = false; if ($data['id']) { $grade_item = new grade_item(array('id' => $data['id'], 'courseid' => $data['courseid'])); } $errors = parent::validation($data, $files); if (array_key_exists('idnumber', $data)) { if ($grade_item) { if ($grade_item->itemtype == 'mod') { $cm = get_coursemodule_from_instance($grade_item->itemmodule, $grade_item->iteminstance, $grade_item->courseid); } else { $cm = null; } } else { $grade_item = null; $cm = null; } if (!grade_verify_idnumber($data['idnumber'], $COURSE->id, $grade_item, $cm)) { $errors['idnumber'] = get_string('idnumbertaken'); } } if (array_key_exists('gradetype', $data) and $data['gradetype'] == GRADE_TYPE_SCALE) { if (empty($data['scaleid'])) { $errors['scaleid'] = get_string('missingscale', 'grades'); } } if (array_key_exists('grademin', $data) and array_key_exists('grademax', $data)) { if ($data['grademax'] == $data['grademin'] or $data['grademax'] < $data['grademin']) { $errors['grademin'] = get_string('incorrectminmax', 'grades'); $errors['grademax'] = get_string('incorrectminmax', 'grades'); } } // We do not want the user to be able to change the grade type or scale for this item if grades exist. if ($grade_item && $grade_item->has_grades()) { // Check that grade type is set - should never not be set unless form has been modified. if (!isset($data['gradetype'])) { $errors['gradetype'] = get_string('modgradecantchangegradetype', 'grades'); } else { if ($data['gradetype'] !== $grade_item->gradetype) { // Check if we are changing the grade type. $errors['gradetype'] = get_string('modgradecantchangegradetype', 'grades'); } else { if ($data['gradetype'] == GRADE_TYPE_SCALE) { // Check if we are changing the scale - can't do this when grades exist. if (isset($data['scaleid']) && $data['scaleid'] !== $grade_item->scaleid) { $errors['scaleid'] = get_string('modgradecantchangescale', 'grades'); } } } } } if ($grade_item) { if ($grade_item->gradetype == GRADE_TYPE_VALUE) { if (grade_floats_different($data['grademin'], $grade_item->grademin) || grade_floats_different($data['grademax'], $grade_item->grademax)) { if ($grade_item->has_grades() && empty($data['rescalegrades'])) { $errors['rescalegrades'] = get_string('mustchooserescaleyesorno', 'grades'); } } } } return $errors; }
/** * Given an array of all assessments done by a single reviewer, calculates the final grading grade * * This calculates the simple mean of the passed grading grades. If, however, the grading grade * was overridden by a teacher, the gradinggradeover value is returned and the rest of grades are ignored. * * @param array $assessments of stdclass(->reviewerid ->gradinggrade ->gradinggradeover ->aggregationid ->aggregatedgrade) * @param null|int $timegraded explicit timestamp of the aggregation, defaults to the current time * @return void */ protected function aggregate_grading_grades_process(array $assessments, $timegraded = null) { global $DB; $reviewerid = null; // the id of the reviewer being processed $current = null; // the gradinggrade currently saved in database $finalgrade = null; // the new grade to be calculated $agid = null; // aggregation id $sumgrades = 0; $count = 0; if (is_null($timegraded)) { $timegraded = time(); } foreach ($assessments as $assessment) { if (is_null($reviewerid)) { // the id is the same in all records, fetch it during the first loop cycle $reviewerid = $assessment->reviewerid; } if (is_null($agid)) { // the id is the same in all records, fetch it during the first loop cycle $agid = $assessment->aggregationid; } if (is_null($current)) { // the currently saved grade is the same in all records, fetch it during the first loop cycle $current = $assessment->aggregatedgrade; } if (!is_null($assessment->gradinggradeover)) { // the grading grade for this assessment is overridden by a teacher $sumgrades += $assessment->gradinggradeover; $count++; } else { if (!is_null($assessment->gradinggrade)) { $sumgrades += $assessment->gradinggrade; $count++; } } } if ($count > 0) { $finalgrade = grade_floatval($sumgrades / $count); } // Event information. $params = array('context' => $this->context, 'courseid' => $this->course->id, 'relateduserid' => $reviewerid); // check if the new final grade differs from the one stored in the database if (grade_floats_different($finalgrade, $current)) { $params['other'] = array('currentgrade' => $current, 'finalgrade' => $finalgrade); // we need to save new calculation into the database if (is_null($agid)) { // no aggregation record yet $record = new stdclass(); $record->workshopid = $this->id; $record->userid = $reviewerid; $record->gradinggrade = $finalgrade; $record->timegraded = $timegraded; $record->id = $DB->insert_record('workshop_aggregations', $record); $params['objectid'] = $record->id; $event = \mod_workshop\event\assessment_evaluated::create($params); $event->trigger(); } else { $record = new stdclass(); $record->id = $agid; $record->gradinggrade = $finalgrade; $record->timegraded = $timegraded; $DB->update_record('workshop_aggregations', $record); $params['objectid'] = $agid; $event = \mod_workshop\event\assessment_reevaluated::create($params); $event->trigger(); } } }
$data->finalgrade = $old_grade_grade->finalgrade; } } } // the overriding of feedback is tricky - we have to care about external items only if (!array_key_exists('feedback', $data) or $data->feedback == $data->oldfeedback) { $data->feedback = $old_grade_grade->feedback; $data->feedbackformat = $old_grade_grade->feedbackformat; } // update final grade or feedback $grade_item->update_final_grade($data->userid, $data->finalgrade, 'editgrade', $data->feedback, $data->feedbackformat); $grade_grade = new grade_grade(array('userid' => $data->userid, 'itemid' => $grade_item->id), true); $grade_grade->grade_item =& $grade_item; // no db fetching if (has_capability('moodle/grade:manage', $context) or has_capability('moodle/grade:edit', $context)) { if (!grade_floats_different($data->finalgrade, $old_grade_grade->finalgrade) and $data->feedback === $old_grade_grade->feedback) { // change overridden flag only if grade or feedback not changed if (!isset($data->overridden)) { $data->overridden = 0; // checkbox } $grade_grade->set_overridden($data->overridden); } } if (has_capability('moodle/grade:manage', $context) or has_capability('moodle/grade:hide', $context)) { $hidden = empty($data->hidden) ? 0 : $data->hidden; $hiddenuntil = empty($data->hiddenuntil) ? 0 : $data->hiddenuntil; if ($grade_item->is_hidden()) { if ($old_grade_grade->hidden == 1 and $hiddenuntil == 0) { //nothing to do - grade was originally hidden, we want to keep it that way } else {
/** * Internal function for category grades summing * * @param grade_grade $grade The grade item * @param float $oldfinalgrade Old Final grade * @param array $items Grade items * @param array $grade_values Grade values * @param array $excluded Excluded */ private function sum_grades(&$grade, $oldfinalgrade, $items, $grade_values, $excluded) { if (empty($items)) { return null; } // ungraded and excluded items are not used in aggregation foreach ($grade_values as $itemid => $v) { if (is_null($v)) { unset($grade_values[$itemid]); } else { if (in_array($itemid, $excluded)) { unset($grade_values[$itemid]); } } } // use 0 if grade missing, droplow used and aggregating all items if (!$this->aggregateonlygraded and !empty($this->droplow)) { foreach ($items as $itemid => $value) { if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) { $grade_values[$itemid] = 0; } } } $this->apply_limit_rules($grade_values, $items); $sum = array_sum($grade_values); $grade->finalgrade = $this->grade_item->bounded_grade($sum); // update in db if changed if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->update('aggregation'); } return; }
/** * Save quick grades. * * @return string The result of the save operation */ protected function process_save_quick_grades() { global $USER, $DB, $CFG; // Need grade permission. require_capability('mod/assign:grade', $this->context); require_sesskey(); // Make sure advanced grading is disabled. $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions'); $controller = $gradingmanager->get_active_controller(); if (!empty($controller)) { return get_string('errorquickgradingvsadvancedgrading', 'assign'); } $users = array(); // First check all the last modified values. $currentgroup = groups_get_activity_group($this->get_course_module(), true); $participants = $this->list_participants($currentgroup, true); // Gets a list of possible users and look for values based upon that. foreach ($participants as $userid => $unused) { $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT); $attemptnumber = optional_param('gradeattempt_' . $userid, -1, PARAM_INT); // Gather the userid, updated grade and last modified value. $record = new stdClass(); $record->userid = $userid; if ($modified >= 0) { $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT)); $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_TEXT); $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT); } else { // This user was not in the grading table. continue; } $record->attemptnumber = $attemptnumber; $record->lastmodified = $modified; $record->gradinginfo = grade_get_grades($this->get_course()->id, 'mod', 'assign', $this->get_instance()->id, array($userid)); $users[$userid] = $record; } if (empty($users)) { return get_string('nousersselected', 'assign'); } list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED); $params['assignid1'] = $this->get_instance()->id; $params['assignid2'] = $this->get_instance()->id; // Check them all for currency. $grademaxattempt = 'SELECT s.userid, s.attemptnumber AS maxattempt FROM {assign_submission} s WHERE s.assignment = :assignid1 AND s.latest = 1'; $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified, uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber FROM {user} u LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid LEFT JOIN {assign_grades} g ON u.id = g.userid AND g.assignment = :assignid2 AND g.attemptnumber = gmx.maxattempt LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid WHERE u.id ' . $userids; $currentgrades = $DB->get_recordset_sql($sql, $params); $modifiedusers = array(); foreach ($currentgrades as $current) { $modified = $users[(int)$current->userid]; $grade = $this->get_user_grade($modified->userid, false); // Check to see if the grade column was even visible. $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false; // Check to see if the outcomes were modified. if ($CFG->enableoutcomes) { foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) { $oldoutcome = $outcome->grades[$modified->userid]->grade; $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid; $newoutcome = optional_param($paramname, -1, PARAM_FLOAT); // Check to see if the outcome column was even visible. $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false; if ($outcomecolpresent && ($oldoutcome != $newoutcome)) { // Can't check modified time for outcomes because it is not reported. $modifiedusers[$modified->userid] = $modified; continue; } } } // Let plugins participate. foreach ($this->feedbackplugins as $plugin) { if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) { // The plugins must handle is_quickgrading_modified correctly - ie // handle hidden columns. if ($plugin->is_quickgrading_modified($modified->userid, $grade)) { if ((int)$current->lastmodified > (int)$modified->lastmodified) { return get_string('errorrecordmodified', 'assign'); } else { $modifiedusers[$modified->userid] = $modified; continue; } } } } if (($current->grade < 0 || $current->grade === null) && ($modified->grade < 0 || $modified->grade === null)) { // Different ways to indicate no grade. $modified->grade = $current->grade; // Keep existing grade. } // Treat 0 and null as different values. if ($current->grade !== null) { $current->grade = floatval($current->grade); } $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade); $markingallocationchanged = $this->get_instance()->markingworkflow && $this->get_instance()->markingallocation && ($modified->allocatedmarker !== false) && ($current->allocatedmarker != $modified->allocatedmarker); $workflowstatechanged = $this->get_instance()->markingworkflow && ($modified->workflowstate !== false) && ($current->workflowstate != $modified->workflowstate); if ($gradechanged || $markingallocationchanged || $workflowstatechanged) { // Grade changed. if ($this->grading_disabled($modified->userid)) { continue; } $badmodified = (int)$current->lastmodified > (int)$modified->lastmodified; $badattempt = (int)$current->attemptnumber != (int)$modified->attemptnumber; if ($badmodified || $badattempt) { // Error - record has been modified since viewing the page. return get_string('errorrecordmodified', 'assign'); } else { $modifiedusers[$modified->userid] = $modified; } } } $currentgrades->close(); $adminconfig = $this->get_admin_config(); $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook; // Ok - ready to process the updates. foreach ($modifiedusers as $userid => $modified) { $grade = $this->get_user_grade($userid, true); $flags = $this->get_user_flags($userid, true); $grade->grade= grade_floatval(unformat_float($modified->grade)); $grade->grader= $USER->id; $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false; // Save plugins data. foreach ($this->feedbackplugins as $plugin) { if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) { $plugin->save_quickgrading_changes($userid, $grade); if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) { // This is the feedback plugin chose to push comments to the gradebook. $grade->feedbacktext = $plugin->text_for_gradebook($grade); $grade->feedbackformat = $plugin->format_for_gradebook($grade); } } } // These will be set to false if they are not present in the quickgrading // form (e.g. column hidden). $workflowstatemodified = ($modified->workflowstate !== false) && ($flags->workflowstate != $modified->workflowstate); $allocatedmarkermodified = ($modified->allocatedmarker !== false) && ($flags->allocatedmarker != $modified->allocatedmarker); if ($workflowstatemodified) { $flags->workflowstate = $modified->workflowstate; } if ($allocatedmarkermodified) { $flags->allocatedmarker = $modified->allocatedmarker; } if ($workflowstatemodified || $allocatedmarkermodified) { if ($this->update_user_flags($flags) && $workflowstatemodified) { $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger(); } } $this->update_grade($grade); // Allow teachers to skip sending notifications. if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) { $this->notify_grade_modified($grade, true); } // Save outcomes. if ($CFG->enableoutcomes) { $data = array(); foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) { $oldoutcome = $outcome->grades[$modified->userid]->grade; $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid; // This will be false if the input was not in the quickgrading // form (e.g. column hidden). $newoutcome = optional_param($paramname, false, PARAM_INT); if ($newoutcome !== false && ($oldoutcome != $newoutcome)) { $data[$outcomeid] = $newoutcome; } } if (count($data) > 0) { grade_update_outcomes('mod/assign', $this->course->id, 'mod', 'assign', $this->get_instance()->id, $userid, $data); } } } return get_string('quickgradingchangessaved', 'assign'); }
/** * Calculates the aggregated grade given by the reviewer * * @param array $grades Grade records as returned by {@link get_current_assessment_data} * @return float|null Raw grade (0.00000 to 100.00000) for submission as suggested by the peer */ protected function calculate_peer_grade(array $grades) { if (empty($grades)) { return null; } $sumerrors = 0; // sum of the weighted errors (ie the negative responses) foreach ($grades as $grade) { if (grade_floats_different($grade->grade, 1.00000)) { // negative reviewer's response $sumerrors += $this->dimensions[$grade->dimensionid]->weight; } } return $this->errors_to_grade($sumerrors); }
/** * Set the flags on the grade_grade items to indicate how individual grades are used * in the aggregation. * * WARNING: This function is called a lot during gradebook recalculation, be very performance considerate. * * @param int $userid The user we have aggregated the grades for. * @param array $usedweights An array with keys for each of the grade_item columns included in the aggregation. The value are the relative weight. * @param array $novalue An array with keys for each of the grade_item columns skipped because * they had no value in the aggregation. * @param array $dropped An array with keys for each of the grade_item columns dropped * because of any drop lowest/highest settings in the aggregation. * @param array $extracredit An array with keys for each of the grade_item columns * considered extra credit by the aggregation. */ private function set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit) { global $DB; // We want to know all current user grades so we can decide whether they need to be updated or they already contain the // expected value. $sql = "SELECT gi.id, gg.aggregationstatus, gg.aggregationweight FROM {grade_grades} gg\n JOIN {grade_items} gi ON (gg.itemid = gi.id)\n WHERE gg.userid = :userid"; $params = array('categoryid' => $this->id, 'userid' => $userid); // These are all grade_item ids which grade_grades will NOT end up being 'unknown' (because they are not unknown or // because we will update them to something different that 'unknown'). $giids = array_keys($usedweights + $novalue + $dropped + $extracredit); if ($giids) { // We include grade items that might not be in categoryid. list($itemsql, $itemlist) = $DB->get_in_or_equal($giids, SQL_PARAMS_NAMED, 'gg'); $sql .= ' AND (gi.categoryid = :categoryid OR gi.id ' . $itemsql . ')'; $params = $params + $itemlist; } else { $sql .= ' AND gi.categoryid = :categoryid'; } $currentgrades = $DB->get_recordset_sql($sql, $params); // We will store here the grade_item ids that need to be updated on db. $toupdate = array(); if ($currentgrades->valid()) { // Iterate through the user grades to see if we really need to update any of them. foreach ($currentgrades as $currentgrade) { // Unset $usedweights that we do not need to update. if (!empty($usedweights) && isset($usedweights[$currentgrade->id]) && $currentgrade->aggregationstatus === 'used') { // We discard the ones that already have the contribution specified in $usedweights and are marked as 'used'. if (grade_floats_equal($currentgrade->aggregationweight, $usedweights[$currentgrade->id])) { unset($usedweights[$currentgrade->id]); } // Used weights can be present in multiple set_usedinaggregation arguments. if (!isset($novalue[$currentgrade->id]) && !isset($dropped[$currentgrade->id]) && !isset($extracredit[$currentgrade->id])) { continue; } } // No value grades. if (!empty($novalue) && isset($novalue[$currentgrade->id])) { if ($currentgrade->aggregationstatus !== 'novalue' || grade_floats_different($currentgrade->aggregationweight, 0)) { $toupdate['novalue'][] = $currentgrade->id; } continue; } // Dropped grades. if (!empty($dropped) && isset($dropped[$currentgrade->id])) { if ($currentgrade->aggregationstatus !== 'dropped' || grade_floats_different($currentgrade->aggregationweight, 0)) { $toupdate['dropped'][] = $currentgrade->id; } continue; } // Extra credit grades. if (!empty($extracredit) && isset($extracredit[$currentgrade->id])) { // If this grade item is already marked as 'extra' and it already has the provided $usedweights value would be // silly to update to 'used' to later update to 'extra'. if (!empty($usedweights) && isset($usedweights[$currentgrade->id]) && grade_floats_equal($currentgrade->aggregationweight, $usedweights[$currentgrade->id])) { unset($usedweights[$currentgrade->id]); } // Update the item to extra if it is not already marked as extra in the database or if the item's // aggregationweight will be updated when going through $usedweights items. if ($currentgrade->aggregationstatus !== 'extra' || !empty($usedweights) && isset($usedweights[$currentgrade->id])) { $toupdate['extracredit'][] = $currentgrade->id; } continue; } // If is not in any of the above groups it should be set to 'unknown', checking that the item is not already // unknown, if it is we don't need to update it. if ($currentgrade->aggregationstatus !== 'unknown' || grade_floats_different($currentgrade->aggregationweight, 0)) { $toupdate['unknown'][] = $currentgrade->id; } } $currentgrades->close(); } // Update items to 'unknown' status. if (!empty($toupdate['unknown'])) { list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['unknown'], SQL_PARAMS_NAMED, 'g'); $itemlist['userid'] = $userid; $sql = "UPDATE {grade_grades}\n SET aggregationstatus = 'unknown',\n aggregationweight = 0\n WHERE itemid {$itemsql} AND userid = :userid"; $DB->execute($sql, $itemlist); } // Update items to 'used' status and setting the proper weight. if (!empty($usedweights)) { // The usedweights items are updated individually to record the weights. foreach ($usedweights as $gradeitemid => $contribution) { $sql = "UPDATE {grade_grades}\n SET aggregationstatus = 'used',\n aggregationweight = :contribution\n WHERE itemid = :itemid AND userid = :userid"; $params = array('contribution' => $contribution, 'itemid' => $gradeitemid, 'userid' => $userid); $DB->execute($sql, $params); } } // Update items to 'novalue' status. if (!empty($toupdate['novalue'])) { list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['novalue'], SQL_PARAMS_NAMED, 'g'); $itemlist['userid'] = $userid; $sql = "UPDATE {grade_grades}\n SET aggregationstatus = 'novalue',\n aggregationweight = 0\n WHERE itemid {$itemsql} AND userid = :userid"; $DB->execute($sql, $itemlist); } // Update items to 'dropped' status. if (!empty($toupdate['dropped'])) { list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['dropped'], SQL_PARAMS_NAMED, 'g'); $itemlist['userid'] = $userid; $sql = "UPDATE {grade_grades}\n SET aggregationstatus = 'dropped',\n aggregationweight = 0\n WHERE itemid {$itemsql} AND userid = :userid"; $DB->execute($sql, $itemlist); } // Update items to 'extracredit' status. if (!empty($toupdate['extracredit'])) { list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['extracredit'], SQL_PARAMS_NAMED, 'g'); $itemlist['userid'] = $userid; $DB->set_field_select('grade_grades', 'aggregationstatus', 'extra', "itemid {$itemsql} AND userid = :userid", $itemlist); } }