Thanks to Tim Hunt for all the help.
Part of MDL-62610
* @return question_usage_by_activity the usage.
*/
public function get_question_usage() {
- if (!PHPUNIT_TEST) {
+ if (!(PHPUNIT_TEST || defined('BEHAT_TEST'))) {
throw new coding_exception('get_question_usage is only for use in unit tests. ' .
'For other operations, use the quiz_attempt api, or extend it properly.');
}
* @param int $timestamp the timestamp that should be stored as the modifed
* time in the database for these actions. If null, will use the current time.
* @param bool $becomingoverdue
- * @param array|null $simulatedresponses If not null, then we are testing, and this is an array of simulated data, keys are slot
- * nos and values are arrays representing student responses which will be passed to
- * question_definition::prepare_simulated_post_data method and then have the
- * appropriate prefix added.
+ * @param array|null $simulatedresponses If not null, then we are testing, and this is an array of simulated data.
+ * There are two formats supported here, for historical reasons. The newer approach is to pass an array created by
+ * {@link core_question_generator::get_simulated_post_data_for_questions_in_usage()}.
+ * the second is to pass an array slot no => contains arrays representing student
+ * responses which will be passed to {@link question_definition::prepare_simulated_post_data()}.
+ * This second method will probably get deprecated one day.
*/
public function process_submitted_actions($timestamp, $becomingoverdue = false, $simulatedresponses = null) {
global $DB;
$transaction = $DB->start_delegated_transaction();
if ($simulatedresponses !== null) {
- $simulatedpostdata = $this->quba->prepare_simulated_post_data($simulatedresponses);
+ if (is_int(key($simulatedresponses))) {
+ // Legacy approach. Should be removed one day.
+ $simulatedpostdata = $this->quba->prepare_simulated_post_data($simulatedresponses);
+ } else {
+ $simulatedpostdata = $simulatedresponses;
+ }
+
} else {
$simulatedpostdata = null;
}
/**
* Prepare and start a new attempt deleting the previous preview attempts.
*
- * @param quiz $quizobj quiz object
- * @param int $attemptnumber the attempt number
- * @param object $lastattempt last attempt object
+ * @param quiz $quizobj quiz object
+ * @param int $attemptnumber the attempt number
+ * @param object $lastattempt last attempt object
* @param bool $offlineattempt whether is an offline attempt or not
+ * @param array $forcedrandomquestions slot number => question id. Used for random questions,
+ * to force the choice of a particular actual question. Intended for testing purposes only.
+ * @param array $forcedvariants slot number => variant. Used for questions with variants,
+ * to force the choice of a particular variant. Intended for testing purposes only.
* @return object the new attempt
* @since Moodle 3.1
*/
-function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $offlineattempt = false) {
+function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $lastattempt,
+ $offlineattempt = false, $forcedrandomquestions = [], $forcedvariants = []) {
global $DB, $USER;
// Delete any previous preview attempts belonging to this user.
$attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow, $quizobj->is_preview_user());
if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) {
- $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow);
+ $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow,
+ $forcedrandomquestions, $forcedvariants);
} else {
$attempt = quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt);
}
return parent::create_instance($record, (array)$options);
}
+
+ /**
+ * Create a quiz attempt for a particular user at a particular course.
+ *
+ * Currently this method can only create a first attempt for each
+ * user at each quiz. TODO remove this limitation.
+ *
+ * @param int $quizid the quiz id (from the mdl_quit table, not cmid).
+ * @param int $userid the user id.
+ * @param array $forcedrandomquestions slot => questionid. Optional,
+ * used with random questions, to control which one is 'randomly' selected in that slot.
+ * @param array $forcedvariants slot => variantno. Optional. Optional,
+ * used with question where get_num_variants is > 1, to control which
+ * variants is 'randomly' selected.
+ * @return stdClass the new attempt.
+ */
+ public function create_attempt($quizid, $userid, array $forcedrandomquestions = [],
+ array $forcedvariants = []) {
+ // Build quiz object and load questions.
+ $quizobj = quiz::create($quizid, $userid);
+
+ if (quiz_get_user_attempts($quizid, $userid, 'all', true)) {
+ throw new coding_exception('mod_quiz_generator is currently limited to only ' .
+ 'be able to create one attempt for each user. (This should be fixed.)');
+ }
+
+ return quiz_prepare_and_start_new_attempt($quizobj, 1, null,
+ $offlineattempt = false, $forcedrandomquestions = [], $forcedvariants);
+ }
+
+ /**
+ * Submit responses to a quiz attempt.
+ *
+ * To be realistic, you should ensure that $USER is set to the user whose attempt
+ * it is before calling this.
+ *
+ * @param int $attemptid the id of the attempt which is being
+ * @param array $responses array responses to submit. See description on
+ * {@link core_question_generator::get_simulated_post_data_for_questions_in_usage()}.
+ * @param bool $finishattempt of true, the attempt will be submitted.
+ */
+ public function submit_responses($attemptid, array $responses, $finishattempt) {
+ /** @var $questiongenerator core_question_generator */
+ $questiongenerator = $this->datagenerator->get_plugin_generator('core_question');
+
+ $attemptobj = quiz_attempt::create($attemptid);
+
+ $postdata = $questiongenerator->get_simulated_post_data_for_questions_in_usage(
+ $attemptobj->get_question_usage(), $responses);
+
+ $attemptobj->process_submitted_actions(time(), false, $postdata);
+
+ if ($finishattempt) {
+ $attemptobj->process_finish(time(), false);
+ }
+ }
}
return array($category, $course, $qcat, $questions);
}
+
+ /**
+ * This method can construct what the post data would be to simulate a user submitting
+ * responses to a number of questions within a question usage.
+ *
+ * In the responses array, the array keys are the slot numbers for which a response will
+ * be submitted. You can submit a response to any number of responses within the usage.
+ * There is no need to do them all. The values are a string representation of the response.
+ * The exact meaning of that depends on the particular question type. These strings
+ * are passed to the un_summarise_response method of the question to decode.
+ *
+ * @param question_usage_by_activity $quba the question usage.
+ * @param array $responses the resonses to submit, in the format described above.
+ * @return array that can be passed to methods like $quba->process_all_actions as simulated POST data.
+ */
+ public function get_simulated_post_data_for_questions_in_usage(
+ question_usage_by_activity $quba, array $responses) {
+ $postdata = [];
+
+ foreach ($responses as $slot => $responsesummary) {
+ $postdata += $this->get_simulated_post_data_for_question_attempt(
+ $quba->get_question_attempt($slot), $responsesummary);
+ }
+
+ return $postdata;
+ }
+
+ /**
+ * This method can construct what the post data would be to simulate a user submitting
+ * responses to one particular question attempt.
+ *
+ * The $responsesummary is a string representation of the response to be submitted.
+ * The exact meaning of that depends on the particular question type. These strings
+ * are passed to the un_summarise_response method of the question to decode.
+ *
+ * @param question_attempt $qa the question attempt for which we are generating POST data.
+ * @param $responsesummary a textual summary of the resonse, as described above.
+ * @return array the sumulated post data that can be passed to $quba->process_all_actions.
+ */
+ public function get_simulated_post_data_for_question_attempt(
+ question_attempt $qa, $responsesummary) {
+
+ $question = $qa->get_question();
+ if (!$question instanceof question_with_responses) {
+ return [];
+ }
+
+ $postdata = [];
+ $postdata[$qa->get_control_field_name('sequencecheck')] = (string)$qa->get_sequence_check_count();
+ $postdata[$qa->get_flag_field_name()] = (string)(int)$qa->is_flagged();
+
+ $response = $question->un_summarise_response($responsesummary);
+ foreach ($response as $name => $value) {
+ $postdata[$qa->get_qt_field_name($name)] = (string)$value;
+ }
+
+ // TODO handle behaviour variables.
+
+ return $postdata;
+ }
}
return null;
}
+ public function un_summarise_response(string $response) {
+ return [];
+ }
+
public function classify_response(array $response) {
return array();
}
/**
* Produce a plain text summary of a response.
- * @param $response a response, as might be passed to {@link grade_response()}.
+ * @param array $response a response, as might be passed to {@link grade_response()}.
* @return string a plain text summary of that response, that could be used in reports.
*/
public function summarise_response(array $response);
+ /**
+ * If possible, construct a response that could have lead to the given
+ * response summary. This is basically the opposite of {@link summarise_response()}
+ * but it is intended only to be used for testing.
+ *
+ * @param string $summary a string, which might have come from summarise_response
+ * @return array a response that could have lead to that.
+ */
+ public function un_summarise_response(string $summary);
+
/**
* Categorise the student's response according to the categories defined by
* get_possible_responses.
public function is_gradable_response(array $response) {
return $this->is_complete_response($response);
}
+
+ public function un_summarise_response(string $summary) {
+ throw new coding_exception('This question type (' . get_class($this) .
+ ' does not implement the un_summarise_response testing method.');
+ }
}
}
}
+ public function un_summarise_response(string $summary) {
+ if ($summary === get_string('true', 'qtype_truefalse')) {
+ return ['answer' => '1'];
+ } else if ($summary === get_string('false', 'qtype_truefalse')) {
+ return ['answer' => '0'];
+ } else {
+ return [];
+ }
+ }
+
public function classify_response(array $response) {
if (!array_key_exists('answer', $response)) {
return array($this->id => question_classified_response::no_response());