Also, cleaned up the marking code for numerical and short answer questions, partly by making the base-class method more useful.
Rewrote the way the way short answer questions do wildcard matching to be more robust, Unicode safe, and generally not suck.
This is a big change, so I am pleased to report I tested it using my new Selinium test suite. See: http://moodle.org/mod/forum/discuss.php?d=51161
$states[$i]->changed = true;
// Create the empty question type specific information
- if (!$QTYPES[$questions[$i]->qtype]
- ->create_session_and_responses($questions[$i], $states[$i], $cmoptions, $attempt)) {
+ if (!$QTYPES[$questions[$i]->qtype]->create_session_and_responses(
+ $questions[$i], $states[$i], $cmoptions, $attempt)) {
return false;
}
$states[$i]->last_graded = clone($states[$i]);
if (question_isgradingevent($action->event)) {
$state->responses = $state->last_graded->responses;
}
+
// Check for unchanged responses (exactly unchanged, not equivalent).
// We also have to catch questions that the student has not yet attempted
- $sameresponses = (($state->responses == $action->responses) or
- ($state->responses == array(''=>'') && array_keys(array_count_values($action->responses))===array('')));
+ $sameresponses = !$state->last_graded->event == QUESTION_EVENTOPEN &&
+ $state->responses == $action->responses;
// If the response has not been changed then we do not have to process it again
// unless the attempt is closing or validation is requested
if ($sameresponses and QUESTION_EVENTCLOSE != $action->event
- and QUESTION_EVENTVALIDATE != $action->event) {
+ and QUESTION_EVENTVALIDATE != $action->event) {
return true;
}
// Unless the attempt is closing, we want to work out if the current responses
// (or equivalent responses) were already given in the last graded attempt.
- if((QUESTION_EVENTCLOSE != $action->event) and $QTYPES[$question->qtype]->compare_responses(
- $question, $state, $state->last_graded)) {
+ if(QUESTION_EVENTCLOSE != $action->event && QUESTION_EVENTOPEN != $state->last_graded->event &&
+ $QTYPES[$question->qtype]->compare_responses($question, $state, $state->last_graded)) {
$state->event = QUESTION_EVENTDUPLICATE;
}
// If we did not find a duplicate or if the attempt is closing, perform grading
- if ((!$sameresponses and (QUESTION_EVENTDUPLICATE != $state->event)) or (QUESTION_EVENTCLOSE == $action->event)) {
+ if ((!$sameresponses and QUESTION_EVENTDUPLICATE != $state->event) or
+ QUESTION_EVENTCLOSE == $action->event) {
// Decrease sumgrades by previous grade and then later add new grade
$attempt->sumgrades -= (float)$state->last_graded->grade;
}
$this->get_numerical_units($question);
- // If units are defined we strip off the defaultunit from the answer, if
+ // If units are defined we strip off the default unit from the answer, if
// it is present. (Required for compatibility with the old code and DB).
if ($defaultunit = $this->get_default_numerical_unit($question)) {
foreach($question->options->answers as $key => $val) {
return true;
}
- function compare_responses(&$question, &$state, &$teststate) {
- $response = isset($state->responses['']) ? $state->responses[''] : '';
- $testresponse = isset($teststate->responses[''])
- ? $teststate->responses[''] : '';
- return ($response == $testresponse);
+ function compare_responses(&$question, $state, $teststate) {
+ if (isset($state->responses['']) && isset($teststate->responses[''])) {
+ return $state->responses[''] == $teststate->responses[''];
+ }
+ return false;
}
/**
return false;
}
- function grade_responses(&$question, &$state, $cmoptions) {
- $answers = &$question->options->answers;
- $state->raw_grade = 0;
- foreach($answers as $answer) {
- if($this->test_response($question, $state, $answer)) {
- $state->raw_grade = $answer->fraction;
- break;
- }
- }
-
- // Make sure we don't assign negative or too high marks
- $state->raw_grade = min(max((float) $state->raw_grade,
- 0.0), 1.0) * $question->maxgrade;
- $state->penalty = $question->penalty * $question->maxgrade;
-
- // mark the state as graded
- $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
-
- return true;
- }
-
function get_correct_responses(&$question, &$state) {
$correct = parent::get_correct_responses($question, $state);
if ($unit = $this->get_default_numerical_unit($question)) {
// will use the empty string '' as the index for that one response. This will
// automatically be stored in and restored from the answer field in the
// question_states table.
- $state->responses = array('' => '');
+ $state->responses = array(
+ '' => '',
+ );
return true;
}
* @param object $cmoptions
*/
function grade_responses(&$question, &$state, $cmoptions) {
- /* The default implementation uses the comparison method to check if
- the responses given are equivalent to the responses for each answer
- in turn and sets the marks and penalty accordingly. This works for the
- most simple question types. */
+ // The default implementation uses the test_response method to
+ // compare what the student entered against each of the possible
+ // answers stored in the question, and uses the grade from the
+ // first one that matches. It also sets the marks and penalty.
+ // This should be good enought for most simple question types.
- $teststate = clone($state);
- $teststate->raw_grade = 0;
+ $state->raw_grade = 0;
foreach($question->options->answers as $answer) {
- $teststate->responses[''] = $answer->answer;
-
- if($this->compare_responses($question, $state, $teststate)) {
- $state->raw_grade = min(max((float) $answer->fraction,
- 0.0), 1.0) * $question->maxgrade;
+ if($this->test_response($question, $state, $answer)) {
+ $state->raw_grade = $answer->fraction;
break;
}
}
- if (empty($state->raw_grade)) {
- $state->raw_grade = 0.0;
- }
- // Only allow one attempt at the question
- $state->penalty = 1;
+
+ // Make sure we don't assign negative or too high marks.
+ $state->raw_grade = min(max((float) $state->raw_grade,
+ 0.0), 1.0) * $question->maxgrade;
+
+ // Update the penalty.
+ $state->penalty = $question->penalty * $question->maxgrade;
// mark the state as graded
$state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
}
function save_question_options($question) {
+ $result = new stdClass;
+
if (!$oldanswers = get_records("question_answers", "question", $question->id, "id ASC")) {
$oldanswers = array();
}
return $result;
}
} else { // This is a completely new answer
- unset($answer);
+ $answer = new stdClass;
$answer->answer = trim($dataanswer);
$answer->question = $question->id;
$answer->fraction = $question->fraction[$key];
/// This implementation is also used by question type 'numerical'
$correctanswers = $this->get_correct_responses($question, $state);
$readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
+ $formatoptions = new stdClass;
$formatoptions->noclean = true;
$formatoptions->para = false;
$nameprefix = $question->name_prefix;
return false;
}
- function grade_responses(&$question, &$state, $cmoptions) {
-
- $teststate = clone($state);
- $state->raw_grade = 0;
- // Compare the response with every teacher answer in turn
- // and return the first one that matches.
- foreach($question->options->answers as $answer) {
- // Now we use a bit of a hack: we put the answer into the response
- // of a teststate so that we can use the function compare_responses()
- $teststate->responses[''] = trim($answer->answer);
- if($this->compare_responses($question, $state, $teststate)) {
- $state->raw_grade = $answer->fraction;
- break;
+ function compare_responses($question, $state, $teststate) {
+ if (isset($state->responses['']) && isset($teststate->responses[''])) {
+ if ($question->options->usecase) {
+ return $state->responses[''] == $teststate->responses[''];
+ } else {
+ return strcasecmp($state->responses[''], $teststate->responses['']) == 0;
}
}
-
- // Make sure we don't assign negative or too high marks
- $state->raw_grade = min(max((float) $state->raw_grade,
- 0.0), 1.0) * $question->maxgrade;
- $state->penalty = $question->penalty * $question->maxgrade;
-
- // mark the state as graded
- $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
-
- return true;
+ return false;
}
- function compare_responses(&$question, &$state, &$teststate) {
- // In this questiontype this function is not only used to compare responses
- // between two different states but it is also used by grade_responses() and
- // by test_responses() to compare responses with answers.
- if (isset($state->responses[''])) {
- $response0 = trim(stripslashes($state->responses['']));
- } else {
- $response0 = '';
- }
-
- if (isset($teststate->responses[''])) {
- $response1 = trim(stripslashes($teststate->responses['']));
- } else {
- $response1 = '';
- }
-
- if (!$question->options->usecase) { // Don't compare case
- $response0 = strtolower($response0);
- $response1 = strtolower($response1);
- }
-
- /// These are things to protect in the strings when wildcards are used
- $search = array('\\', '+', '(', ')', '[', ']', '-');
- $replace = array('\\\\', '\+', '\(', '\)', '\[', '\]', '\-');
-
- if (strpos(' '.$response1, '*')) {
- $response1 = str_replace('\*','@@@@@@',$response1);
- $response1 = str_replace('*','.*',$response1);
- $response1 = str_replace($search, $replace, $response1);
- $response1 = str_replace('@@@@@@', '\*',$response1);
-
- if (ereg('^'.$response1.'$', $response0)) {
- return true;
- }
-
- } else if ($response1 == $response0) {
- return true;
- }
-
- return false;
+ function test_response(&$question, $state, $answer) {
+ return $this->compare_string_with_wildcard($state->responses[''],
+ $answer->answer, !$question->options->usecase);
}
- function test_response(&$question, &$state, &$answer) {
- $teststate = clone($state);
- $teststate->responses[''] = trim($answer->answer);
- if($this->compare_responses($question, $state, $teststate)) {
- return true;
- }
- return false;
+ function compare_string_with_wildcard($string, $pattern, $ignorecase) {
+ // Break the string on non-escaped asterisks.
+ $bits = preg_split('/(?<!\\\\)\*/', $pattern);
+ // Escape regexp special characters in the bits.
+ $bits = array_map('preg_quote', $bits);
+ // Put it back together to make the regexp.
+ $regexp = '|^' . implode('.*', $bits) . '$|u';
+
+ // Make the match insensitive if requested to.
+ if ($ignorecase) {
+ $regexp .= 'i';
+ }
+
+ return preg_match($regexp, trim($string));
}
/// BACKUP FUNCTIONS ////////////////////////////
$sho_info = $shortanswers[$i];
//Now, build the question_shortanswer record structure
+ $shortanswer = new stdClass;
$shortanswer->question = $new_question_id;
$shortanswer->answers = backup_todb($sho_info['#']['ANSWERS']['0']['#']);
$shortanswer->usecase = backup_todb($sho_info['#']['USECASE']['0']['#']);