/** * For a given question in an attempt we walk the complete history of states * and recalculate the grades as we go along. * * This is used when a question is changed and old student * responses need to be marked with the new version of a question. * * TODO: Make sure this is not quiz-specific * * @return boolean Indicates whether the grade has changed * @param object $question A question object * @param object $attempt The attempt, in which the question needs to be regraded. * @param object $cmoptions * @param boolean $verbose Optional. Whether to print progress information or not. * @param boolean $dryrun Optional. Whether to make changes to grades records * or record that changes need to be made for a later regrade. */ function regrade_question_in_attempt($question, $attempt, $cmoptions, $verbose = false, $dryrun = false) { global $DB; // load all states for this question in this attempt, ordered in sequence if ($states = $DB->get_records('question_states', array('attempt' => $attempt->uniqueid, 'question' => $question->id), 'seq_number ASC')) { $states = array_values($states); // Subtract the grade for the latest state from $attempt->sumgrades to get the // sumgrades for the attempt without this question. $attempt->sumgrades -= $states[count($states) - 1]->grade; // Initialise the replaystate $replaystate = question_load_specific_state($question, $cmoptions, $attempt, $states[0]->id); $replaystate->sumpenalty = 0; $replaystate->last_graded->sumpenalty = 0; $changed = false; for ($j = 1; $j < count($states); $j++) { restore_question_state($question, $states[$j]); $action = new stdClass(); $action->responses = $states[$j]->responses; $action->timestamp = $states[$j]->timestamp; // Change event to submit so that it will be reprocessed if (in_array($states[$j]->event, array(QUESTION_EVENTCLOSE, QUESTION_EVENTGRADE, QUESTION_EVENTCLOSEANDGRADE))) { $action->event = QUESTION_EVENTSUBMIT; // By default take the event that was saved in the database } else { $action->event = $states[$j]->event; } if ($action->event == QUESTION_EVENTMANUALGRADE) { // Ensure that the grade is in range - in the past this was not checked, // but now it is (MDL-14835) - so we need to ensure the data is valid before // proceeding. if ($states[$j]->grade < 0) { $states[$j]->grade = 0; $changed = true; } else { if ($states[$j]->grade > $question->maxgrade) { $states[$j]->grade = $question->maxgrade; $changed = true; } } if (!$dryrun) { $error = question_process_comment($question, $replaystate, $attempt, $replaystate->manualcomment, $states[$j]->grade); if (is_string($error)) { notify($error); } } else { $replaystate->grade = $states[$j]->grade; } } else { // Reprocess (regrade) responses if (!question_process_responses($question, $replaystate, $action, $cmoptions, $attempt) && $verbose) { $a = new stdClass(); $a->qid = $question->id; $a->stateid = $states[$j]->id; notify(get_string('errorduringregrade', 'question', $a)); } // We need rounding here because grades in the DB get truncated // e.g. 0.33333 != 0.3333333, but we want them to be equal here if (round((double) $replaystate->raw_grade, 5) != round((double) $states[$j]->raw_grade, 5) or round((double) $replaystate->penalty, 5) != round((double) $states[$j]->penalty, 5) or round((double) $replaystate->grade, 5) != round((double) $states[$j]->grade, 5)) { $changed = true; } // If this was previously a closed state, and it has been knoced back to // graded, then fix up the state again. if ($replaystate->event == QUESTION_EVENTGRADE && ($states[$j]->event == QUESTION_EVENTCLOSE || $states[$j]->event == QUESTION_EVENTCLOSEANDGRADE)) { $replaystate->event = $states[$j]->event; } } $replaystate->id = $states[$j]->id; $replaystate->changed = true; $replaystate->update = true; // This will ensure that the existing database entry is updated rather than a new one created if (!$dryrun) { save_question_session($question, $replaystate); } } if ($changed) { if (!$dryrun) { // TODO, call a method in quiz to do this, where 'quiz' comes from // the question_attempts table. $DB->update_record('quiz_attempts', $attempt); } } if ($changed) { $toinsert = new object(); $toinsert->oldgrade = round((double) $states[count($states) - 1]->grade, 5); $toinsert->newgrade = round((double) $replaystate->grade, 5); $toinsert->attemptid = $attempt->uniqueid; $toinsert->questionid = $question->id; //the grade saved is the old grade if the new grade is saved //it is the new grade if this is a dry run. $toinsert->regraded = $dryrun ? 0 : 1; $toinsert->timemodified = time(); $DB->insert_record('quiz_question_regrade', $toinsert); return true; } else { return false; } } return false; }
/** * Load a particular state of a particular question. Used by the reviewquestion.php * script to let the teacher walk through the entire sequence of a student's * interaction with a question. * * @param $questionid the question id * @param $stateid the id of the particular state to load. */ public function load_specific_question_state($questionid, $stateid) { global $DB; $state = question_load_specific_state($this->questions[$questionid], $this->quiz, $this->attempt, $stateid); if ($state === false) { throw new moodle_quiz_exception($this, 'invalidstateid'); } $this->states[$questionid] = $state; }