MDL-63185 mod_quiz: change APIs to handle attempts on behat
Thanks to Tim Hunt for all the help. Part of MDL-62610
This commit is contained in:
+14
-6
@@ -1101,7 +1101,7 @@ class quiz_attempt {
|
||||
* @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.');
|
||||
}
|
||||
@@ -1814,10 +1814,12 @@ class quiz_attempt {
|
||||
* @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;
|
||||
@@ -1825,7 +1827,13 @@ class quiz_attempt {
|
||||
$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;
|
||||
}
|
||||
|
||||
+11
-5
@@ -2373,14 +2373,19 @@ function quiz_validate_new_attempt(quiz $quizobj, quiz_access_manager $accessman
|
||||
/**
|
||||
* 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.
|
||||
@@ -2394,7 +2399,8 @@ function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $last
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -95,4 +95,60 @@ class mod_quiz_generator extends testing_module_generator {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,4 +154,64 @@ class core_question_generator extends component_generator_base {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@ class qtype_missingtype_question extends question_definition
|
||||
return null;
|
||||
}
|
||||
|
||||
public function un_summarise_response(string $response) {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function classify_response(array $response) {
|
||||
return array();
|
||||
}
|
||||
|
||||
@@ -505,11 +505,21 @@ interface question_manually_gradable {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -641,6 +651,11 @@ abstract class question_with_responses extends question_definition
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -59,6 +59,16 @@ class qtype_truefalse_question extends question_graded_automatically {
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
Reference in New Issue
Block a user