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:
Simey Lameze
2018-09-28 11:07:21 +08:00
parent 35aa9ade76
commit 5c0654d7ff
7 changed files with 171 additions and 12 deletions
+14 -6
View File
@@ -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
View File
@@ -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);
}
+56
View File
@@ -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);
}
}
}
+60
View File
@@ -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;
}
}
+4
View File
@@ -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();
}
+16 -1
View File
@@ -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.');
}
}
+10
View File
@@ -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());