Index: mod/quiz/attemptlib.php =================================================================== RCS file: /cvsroot/moodle/mod/quiz/attemptlib.php,v retrieving revision 1.76 diff -u -r1.76 attemptlib.php --- mod/quiz/attemptlib.php 16 Mar 2011 08:46:31 -0000 1.76 +++ mod/quiz/attemptlib.php 6 Apr 2011 07:17:15 -0000 @@ -103,6 +103,17 @@ if (!$quiz = $DB->get_record('quiz', array('id' => $quizid))) { throw new moodle_exception('invalidquizid', 'quiz'); } + + /// Get previous attempts of this user + /// And also we will need 'uniqueid's of all previous attempts to avoid using one question twice + $previousattempts = array_keys($DB->get_records_select('quiz_attempts', "quiz = '$quizid' AND " . + "userid = '$userid' AND timefinish > 0 AND preview != 1", null, '', 'uniqueid')); + $numberofpreviousattempts = count($previousattempts); + + // MDL-6340: random question should know old attempts of this user to avoid using one question twice + $previousattemptslist = implode(',', $previousattempts); + $quiz->previousattempts = $previousattemptslist; + if (!$course = $DB->get_record('course', array('id' => $quiz->course))) { throw new moodle_exception('invalidcoursemodule'); } Index: question/type/random/questiontype.php =================================================================== RCS file: /cvsroot/moodle/question/type/random/questiontype.php,v retrieving revision 1.42 diff -u -r1.42 questiontype.php --- question/type/random/questiontype.php 9 Mar 2011 08:20:43 -0000 1.42 +++ question/type/random/questiontype.php 6 Apr 2011 07:20:10 -0000 @@ -148,9 +148,10 @@ * @param integer $categoryid the id of a question category. * @param boolean whether to include questions from subcategories. * @param string $questionsinuse comma-separated list of question ids to exclude from consideration. - * @return array of question records. + * @param string $avoidquestionsfromattempts comma-separated list of attempt ids of this user. + * @return array of question ids with less used questions placed last. */ - function get_usable_questions_from_category($categoryid, $subcategories, $questionsinuse) { + function get_usable_questions_from_category($categoryid, $subcategories, $questionsinuse, $avoidquestionsfromattempts) { global $DB; $this->init_qtype_lists(); if ($subcategories) { @@ -158,15 +159,48 @@ } else { $categorylist = $categoryid; } - if (!$catrandoms = $DB->get_records_select('question', - "category IN ($categorylist) - AND parent = 0 - AND hidden = 0 - AND id NOT IN ($questionsinuse) - AND qtype NOT IN ($this->excludedqtypes)", null, '', 'id')) { - $catrandoms = array(); + + if($avoidquestionsfromattempts === ''){ + /// If this is first attempt for this user we simply get all question id's + /// that can be used in this random + if (!$catrandoms = $DB->get_records_select('question', + "category IN ($categorylist) + AND parent = 0 + AND hidden = 0 + AND id NOT IN ($questionsinuse) + AND qtype NOT IN ($this->excludedqtypes)", null, '', 'id')) { + $catrandoms = array(); + } + return swapshuffle(array_keys($catrandoms)); + } else { + /// Get list of usable questions with question id's as keys and + /// number of previous uses by this user as value + list($sqlcategorylist,$paramscategorylist) = $DB->get_in_or_equal($categorylist); + list($sqlquestionsinuse,$paramsquestionsinuse) = $DB->get_in_or_equal($questionsinuse, SQL_PARAMS_QM, 'param0000', false); + list($sqlexcludedqtypes,$paramsexcludedqtypes) = $DB->get_in_or_equal($this->excludedqtypes, SQL_PARAMS_QM, 'param0000', false); + list($sqlavoidquestionsfromattempts,$paramsavoidquestionsfromattempts) = $DB->get_in_or_equal($avoidquestionsfromattempts); + $previoususes = $DB->get_records_sql("SELECT q.id, COUNT(qst.id) AS numprevioususes + FROM {question} q + LEFT JOIN {question_states} qst ON qst.answer LIKE CONCAT('random', q.id, '-%') + WHERE q.category $sqlcategorylist + AND q.parent = 0 + AND q.hidden = 0 + AND q.id $sqlquestionsinuse + AND q.qtype $sqlexcludedqtypes + AND ((qst.attempt IS NULL) OR (qst.attempt $sqlavoidquestionsfromattempts)) + AND ((qst.seq_number IS NULL) OR (qst.seq_number = 0)) + GROUP BY q.id + ORDER BY numprevioususes ASC", array_merge($paramscategorylist, $paramsquestionsinuse, $paramsexcludedqtypes, $paramsavoidquestionsfromattempts)); + /// Since no question can be used twice in one attempt and we are trying hard to use + /// less used questions first there should be at most two subsequent numbers of previous uses. + $minusedcount = current($previoususes); + $minused = array_keys($previoususes, $minusedcount); + $maxused = array_keys($previoususes, $minusedcount + 1); + + /// Now we should reshuffle questions with each number of uses separately and merge them + /// in array of question ids placing less used questions last. + return array_merge(swapshuffle($maxused), swapshuffle($minused)); } - return $catrandoms; } function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) { @@ -184,16 +218,16 @@ if (!isset($this->catrandoms[$question->category][$question->questiontext])) { $catrandoms = $this->get_usable_questions_from_category($question->category, - $question->questiontext == "1", $cmoptions->questionsinuse); - $this->catrandoms[$question->category][$question->questiontext] = swapshuffle_assoc($catrandoms); + $question->questiontext == "1", $cmoptions->questionsinuse, $cmoptions->previousattempts); + $this->catrandoms[$question->category][$question->questiontext] = $catrandoms; } while ($wrappedquestion = array_pop( $this->catrandoms[$question->category][$question->questiontext])) { - if (!preg_match("~(^|,)$wrappedquestion->id(,|$)~", $cmoptions->questionsinuse)) { + if (!strpos(','.$cmoptions->questionsinuse.',', ','.$wrappedquestion.',')) { /// $randomquestion is not in use and will therefore be used /// as the randomquestion here... - $wrappedquestion = $DB->get_record('question', array('id' => $wrappedquestion->id)); + $wrappedquestion = $DB->get_record('question', array('id' => $wrappedquestion)); global $QTYPES; $QTYPES[$wrappedquestion->qtype] ->get_question_options($wrappedquestion);