/** * 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. */ function regrade_question_in_attempt($question, $attempt, $cmoptions, $verbose = false) { // load all states for this question in this attempt, ordered in sequence if ($states = get_records_select('question_states', "attempt = '{$attempt->uniqueid}' AND 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 $state = clone $states[0]; $state->manualcomment = get_field('question_sessions', 'manualcomment', 'attemptid', $attempt->uniqueid, 'questionid', $question->id); restore_question_state($question, $state); $state->sumpenalty = 0.0; $replaystate = clone $state; $replaystate->last_graded = $state; $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 (QUESTION_EVENTCLOSE == $states[$j]->event or QUESTION_EVENTGRADE == $states[$j]->event or QUESTION_EVENTCLOSEANDGRADE == $states[$j]->event) { $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; } else { if ($states[$j]->grade > $question->maxgrade) { $states[$j]->grade = $question->maxgrade; } } $error = question_process_comment($question, $replaystate, $attempt, $replaystate->manualcomment, $states[$j]->grade); if (is_string($error)) { notify($error); } } else { // Reprocess (regrade) responses if (!question_process_responses($question, $replaystate, $action, $cmoptions, $attempt)) { $verbose && notify("Couldn't regrade state #{$state->id}!"); } } // 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; } $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 save_question_session($question, $replaystate); } if ($changed) { // TODO, call a method in quiz to do this, where 'quiz' comes from // the question_attempts table. update_record('quiz_attempts', $attempt); } return $changed; } return false; }
function other_cols($colname, $attempt) { global $QTYPES, $OUTPUT; static $states = array(); if (preg_match('/^qsanswer([0-9]+)$/', $colname, $matches)) { if ($attempt->uniqueid == 0) { return '-'; } $questionid = $matches[1]; if (isset($this->gradedstatesbyattempt[$attempt->uniqueid][$questionid])) { $stateforqinattempt = $this->gradedstatesbyattempt[$attempt->uniqueid][$questionid]; } else { return '-'; } $question = $this->questions[$questionid]; restore_question_state($question, $stateforqinattempt); if (!$this->is_downloading() || $this->is_downloading() == 'xhtml') { $formathtml = true; } else { $formathtml = false; } $summary = $QTYPES[$question->qtype]->response_summary($question, $stateforqinattempt, QUIZ_REPORT_RESPONSES_MAX_LEN_TO_DISPLAY, $formathtml); if (!$this->is_downloading()) { if ($summary) { $link = html_link::make("/mod/quiz/reviewquestion.php?attempt={$attempt->attempt}&question={$question->id}", $summary); $link->add_action(new popup_action('click', $link->url, 'reviewquestion', array('height' => 450, 'width' => 650))); $link->title = $question->formattedname; $summary = $OUTPUT->link($link); if (question_state_is_graded($stateforqinattempt) && $question->maxgrade > 0) { $grade = $stateforqinattempt->grade / $question->maxgrade; $qclass = question_get_feedback_class($grade); $feedbackimg = question_get_feedback_image($grade); $questionclass = "que"; return "<span class=\"{$questionclass}\"><span class=\"{$qclass}\">" . $summary . "</span></span>{$feedbackimg}"; } else { return $summary; } } else { return ''; } } else { return $summary; } } else { return NULL; } }
/** * 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; }
$strquizzes = get_string('modulenameplural', 'quiz'); $question->maxgrade = get_field('quiz_question_instances', 'grade', 'quiz', $quiz->id, 'question', $question->id); // Some of the questions code is optimised to work with several questions // at once so it wants the question to be in an array. $questions = array($question->id => &$question); // Add additional questiontype specific information to the question objects. if (!get_question_options($questions)) { error("Unable to load questiontype specific question information"); } $baseurl = $CFG->wwwroot . '/mod/quiz/reviewquestion.php?question=' . $question->id . '&number=' . $number . '&attempt='; $quiz->thispageurl = $baseurl . $attempt->id; $quiz->cmid = $cm->id; $session = get_record('question_sessions', 'attemptid', $attempt->uniqueid, 'questionid', $question->id); $state->sumpenalty = $session->sumpenalty; $state->manualcomment = $session->manualcomment; restore_question_state($question, $state); $state->last_graded = $state; $options = quiz_get_reviewoptions($quiz, $attempt, $context); $options->validation = $state->event == QUESTION_EVENTVALIDATE; $options->history = (has_capability('mod/quiz:viewreports', $context) and !$attempt->preview) ? 'all' : 'graded'; $questionids = array($question->id); $states = array($question->id => &$state); $headtags = get_html_head_contributions($questionids, $questions, $states); print_header('', '', '', '', $headtags); echo '<div id="overDiv" style="position:absolute; visibility:hidden; z-index:1000;"></div>'; // for overlib /// Print heading print_heading(format_string($question->name)); /// Print infobox $table->align = array("right", "left"); if ($attempt->userid != $USER->id) {
// Find the previous attempt if (!($lastattemptid = get_field('quiz_attempts', 'uniqueid', 'quiz', $attempt->quiz, 'userid', $attempt->userid, 'attempt', $attempt->attempt - 1))) { $sloodle->response->quick_output(-701, 'QUIZ', 'Could not find previous attempt to build on.', FALSE); exit; } // For each question find the responses from the previous attempt and save them to the new session foreach ($questions as $i => $question) { // Load the last graded state for the question $statefields = 'n.questionid as question, s.*, n.sumpenalty'; $sql = "SELECT {$statefields}" . " FROM {$CFG->prefix}question_states s," . " {$CFG->prefix}question_sessions n" . " WHERE s.id = n.newgraded" . " AND n.attemptid = '{$lastattemptid}'" . " AND n.questionid = '{$i}'"; if (!($laststate = get_record_sql($sql))) { // Only restore previous responses that have been graded continue; } // Restore the state so that the responses will be restored restore_question_state($questions[$i], $laststate); // prepare the previous responses for new processing $action = new stdClass(); $action->responses = $laststate->responses; $action->timestamp = $laststate->timestamp; $action->event = QUESTION_EVENTOPEN; // Process these responses ... question_process_responses($questions[$i], $states[$i], $action, $quiz, $attempt); // Fix for Bug #5506: When each attempt is built on the last one, // preserve the options from any previous attempt. if (isset($laststate->options)) { $states[$i]->options = $laststate->options; } // ... and save the new states save_question_session($questions[$i], $states[$i]); }
/** * Get the data for the individual question response analysis table. */ function _process_actual_responses($question, $state) { global $QTYPES; if ($question->qtype != 'random' && $QTYPES[$question->qtype]->show_analysis_of_responses()) { $restoredstate = clone $state; restore_question_state($question, $restoredstate); $responsedetails = $QTYPES[$question->qtype]->get_actual_response_details($question, $restoredstate); foreach ($responsedetails as $responsedetail) { $responsedetail->questionid = $question->id; $this->add_response_detail_to_array($responsedetail); } } }
function other_cols($colname, $attempt) { global $QTYPES; static $states = array(); if (preg_match('/^qsanswer([0-9]+)$/', $colname, $matches)) { if ($attempt->uniqueid == 0) { return '-'; } $questionid = $matches[1]; if (isset($this->gradedstatesbyattempt[$attempt->uniqueid][$questionid])) { $stateforqinattempt = $this->gradedstatesbyattempt[$attempt->uniqueid][$questionid]; } else { return '-'; } $question = $this->questions[$questionid]; restore_question_state($question, $stateforqinattempt); if (!$this->is_downloading() || $this->is_downloading() == 'xhtml') { $formathtml = true; } else { $formathtml = false; } $summary = $QTYPES[$question->qtype]->response_summary($question, $stateforqinattempt, QUIZ_REPORT_RESPONSES_MAX_LEN_TO_DISPLAY, $formathtml); if (!$this->is_downloading()) { if ($summary) { $summary = link_to_popup_window('/mod/quiz/reviewquestion.php?attempt=' . $attempt->attempt . '&question=' . $question->id, 'reviewquestion', $summary, 450, 650, get_string('reviewresponse', 'quiz'), 'none', true); if (question_state_is_graded($stateforqinattempt) && $question->maxgrade > 0) { $grade = $stateforqinattempt->grade / $question->maxgrade; $qclass = question_get_feedback_class($grade); $feedbackimg = question_get_feedback_image($grade); $questionclass = "que"; return "<span class=\"{$questionclass}\"><span class=\"{$qclass}\">" . $summary . "</span></span>{$feedbackimg}"; } else { return $summary; } } else { return ''; } } else { return $summary; } } else { return NULL; } }