/**
  * Test the event.
  */
 public function test_event()
 {
     global $CFG;
     require_once "{$CFG->libdir}/gradelib.php";
     $this->resetAfterTest();
     $course = $this->getDataGenerator()->create_course();
     $user = $this->getDataGenerator()->create_user();
     $this->getDataGenerator()->enrol_user($user->id, $course->id);
     $grade_category = grade_category::fetch_course_category($course->id);
     $grade_category->load_grade_item();
     $grade_item = $grade_category->grade_item;
     $grade_item->update_final_grade($user->id, 10, 'gradebook');
     $grade_grade = new grade_grade(array('userid' => $user->id, 'itemid' => $grade_item->id), true);
     $grade_grade->grade_item = $grade_item;
     $event = \core\event\user_graded::create_from_grade($grade_grade);
     $this->assertEventLegacyLogData(array($course->id, 'grade', 'update', '/report/grader/index.php?id=' . $course->id, $grade_item->itemname . ': ' . fullname($user)), $event);
     $this->assertEquals(context_course::instance($course->id), $event->get_context());
     $this->assertSame($event->objecttable, 'grade_grades');
     $this->assertEquals($event->objectid, $grade_grade->id);
     $this->assertEquals($event->other['itemid'], $grade_item->id);
     $this->assertTrue($event->other['overridden']);
     $this->assertEquals(10, $event->other['finalgrade']);
     // Trigger the events.
     $sink = $this->redirectEvents();
     $event->trigger();
     $result = $sink->get_events();
     $sink->close();
     $this->assertCount(1, $result);
     $event = reset($result);
     $this->assertEventContextNotUsed($event);
     $grade = $event->get_grade();
     $this->assertInstanceOf('grade_grade', $grade);
     $this->assertEquals($grade_grade->id, $grade->id);
 }
Exemple #2
0
 /**
  * Processes the data sent by the form (grades and feedbacks).
  * Caller is responsible for all access control checks
  * @param array $data form submission (with magic quotes)
  * @return array empty array if success, array of warnings if something fails.
  */
 public function process_data($data)
 {
     global $DB;
     $warnings = array();
     $separategroups = false;
     $mygroups = array();
     if ($this->groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $this->context)) {
         $separategroups = true;
         $mygroups = groups_get_user_groups($this->course->id);
         $mygroups = $mygroups[0];
         // ignore groupings
         // reorder the groups fro better perf below
         $current = array_search($this->currentgroup, $mygroups);
         if ($current !== false) {
             unset($mygroups[$current]);
             array_unshift($mygroups, $this->currentgroup);
         }
     }
     // always initialize all arrays
     $queue = array();
     $this->load_users();
     $this->load_final_grades();
     // Were any changes made?
     $changedgrades = false;
     $timepageload = clean_param($data->timepageload, PARAM_INT);
     foreach ($data as $varname => $students) {
         $needsupdate = false;
         // skip, not a grade nor feedback
         if (strpos($varname, 'grade') === 0) {
             $datatype = 'grade';
         } else {
             if (strpos($varname, 'feedback') === 0) {
                 $datatype = 'feedback';
             } else {
                 continue;
             }
         }
         foreach ($students as $userid => $items) {
             $userid = clean_param($userid, PARAM_INT);
             foreach ($items as $itemid => $postedvalue) {
                 $itemid = clean_param($itemid, PARAM_INT);
                 // Was change requested?
                 $oldvalue = $this->grades[$userid][$itemid];
                 if ($datatype === 'grade') {
                     // If there was no grade and there still isn't
                     if (is_null($oldvalue->finalgrade) && $postedvalue == -1) {
                         // -1 means no grade
                         continue;
                     }
                     // If the grade item uses a custom scale
                     if (!empty($oldvalue->grade_item->scaleid)) {
                         if ((int) $oldvalue->finalgrade === (int) $postedvalue) {
                             continue;
                         }
                     } else {
                         // The grade item uses a numeric scale
                         // Format the finalgrade from the DB so that it matches the grade from the client
                         if ($postedvalue === format_float($oldvalue->finalgrade, $oldvalue->grade_item->get_decimals())) {
                             continue;
                         }
                     }
                     $changedgrades = true;
                 } else {
                     if ($datatype === 'feedback') {
                         // If quick grading is on, feedback needs to be compared without line breaks.
                         if ($this->get_pref('quickgrading')) {
                             $oldvalue->feedback = preg_replace("/\r\n|\r|\n/", "", $oldvalue->feedback);
                         }
                         if ($oldvalue->feedback === $postedvalue or $oldvalue->feedback === null and empty($postedvalue)) {
                             continue;
                         }
                     }
                 }
                 if (!($gradeitem = grade_item::fetch(array('id' => $itemid, 'courseid' => $this->courseid)))) {
                     print_error('invalidgradeitemid');
                 }
                 // Pre-process grade
                 if ($datatype == 'grade') {
                     $feedback = false;
                     $feedbackformat = false;
                     if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
                         if ($postedvalue == -1) {
                             // -1 means no grade
                             $finalgrade = null;
                         } else {
                             $finalgrade = $postedvalue;
                         }
                     } else {
                         $finalgrade = unformat_float($postedvalue);
                     }
                     $errorstr = '';
                     $skip = false;
                     $dategraded = $oldvalue->get_dategraded();
                     if (!empty($dategraded) && $timepageload < $dategraded) {
                         // Warn if the grade was updated while we were editing this form.
                         $errorstr = 'gradewasmodifiedduringediting';
                         $skip = true;
                     } else {
                         if (!is_null($finalgrade)) {
                             // Warn if the grade is out of bounds.
                             $bounded = $gradeitem->bounded_grade($finalgrade);
                             if ($bounded > $finalgrade) {
                                 $errorstr = 'lessthanmin';
                             } else {
                                 if ($bounded < $finalgrade) {
                                     $errorstr = 'morethanmax';
                                 }
                             }
                         }
                     }
                     if ($errorstr) {
                         $userfields = 'id, ' . get_all_user_name_fields(true);
                         $user = $DB->get_record('user', array('id' => $userid), $userfields);
                         $gradestr = new stdClass();
                         $gradestr->username = fullname($user);
                         $gradestr->itemname = $gradeitem->get_name();
                         $warnings[] = get_string($errorstr, 'grades', $gradestr);
                         if ($skip) {
                             // Skipping the update of this grade it failed the tests above.
                             continue;
                         }
                     }
                 } else {
                     if ($datatype == 'feedback') {
                         $finalgrade = false;
                         $trimmed = trim($postedvalue);
                         if (empty($trimmed)) {
                             $feedback = null;
                         } else {
                             $feedback = $postedvalue;
                         }
                     }
                 }
                 // group access control
                 if ($separategroups) {
                     // note: we can not use $this->currentgroup because it would fail badly
                     //       when having two browser windows each with different group
                     $sharinggroup = false;
                     foreach ($mygroups as $groupid) {
                         if (groups_is_member($groupid, $userid)) {
                             $sharinggroup = true;
                             break;
                         }
                     }
                     if (!$sharinggroup) {
                         // either group membership changed or somebody is hacking grades of other group
                         $warnings[] = get_string('errorsavegrade', 'grades');
                         continue;
                     }
                 }
                 $oldgradegrade = new grade_grade(array('userid' => $userid, 'itemid' => $gradeitem->id), true);
                 $gradeitem->update_final_grade($userid, $finalgrade, 'gradebook', $feedback, FORMAT_MOODLE);
                 $gradegrade = new grade_grade(array('userid' => $userid, 'itemid' => $gradeitem->id), true);
                 if ($oldgradegrade->finalgrade != $gradegrade->finalgrade or empty($oldgradegrade->overridden) != empty($gradegrade->overridden)) {
                     $gradegrade->grade_item = $gradeitem;
                     \core\event\user_graded::create_from_grade($gradegrade)->trigger();
                 }
                 // We can update feedback without reloading the grade item as it doesn't affect grade calculations
                 if ($datatype === 'feedback') {
                     $this->grades[$userid][$itemid]->feedback = $feedback;
                 }
             }
         }
     }
     if ($changedgrades) {
         // If a final grade was overriden reload grades so dependent grades like course total will be correct
         $this->grades = null;
     }
     return $warnings;
 }
Exemple #3
0
 /**
  * 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;
     }
 }
Exemple #4
0
 /**
  * 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;
 }
     } else {
         $json_object->gradevalue = $finalvalue;
         $old_grade_grade = new grade_grade(array('userid' => $userid, 'itemid' => $grade_item->id), true);
         if ($grade_item->update_final_grade($userid, $finalgrade, 'gradebook', $feedback, FORMAT_MOODLE)) {
             $json_object->result = 'success';
             $json_object->message = false;
         } else {
             $json_object->result = 'error';
             $json_object->message = "TO BE LOCALISED: Failure to update final grade!";
             echo json_encode($json_object);
             die;
         }
         $grade_grade = new grade_grade(array('userid' => $userid, 'itemid' => $grade_item->id), true);
         if ($old_grade_grade->finalgrade != $grade_grade->finalgrade or empty($old_grade_grade->overridden) != empty($grade_grade->overridden)) {
             $grade_grade->load_grade_item();
             \core\event\user_graded::create_from_grade($grade_grade)->trigger();
         }
         // Get row data
         $sql = "SELECT gg.id, gi.id AS itemid, gi.scaleid AS scale, gg.userid AS userid, finalgrade, gg.overridden AS overridden " . "FROM {grade_grades} gg, {grade_items} gi WHERE " . "gi.courseid = ? AND gg.itemid = gi.id AND gg.userid = ?";
         $records = $DB->get_records_sql($sql, array($courseid, $userid));
         $json_object->row = $records;
         echo json_encode($json_object);
         die;
     }
 } else {
     $json_object = new stdClass();
     $json_object->result = "error";
     $json_object->message = "Missing parameter to ajax UPDATE callback: \n" . "  userid: {$userid},\n  itemid: {$itemid}\n,  type: {$type}\n,  newvalue: {$newvalue}";
     echo json_encode($json_object);
 }
 break;
Exemple #6
0
 /**
  * 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();
         $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;
     }
 }