From e49ccc92c450a0da2db146824e7e9b9fe509152f Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Mon, 21 Sep 2020 23:43:24 +0200 Subject: [PATCH] MDL-54956 quiz: Return response files in attempts --- mod/quiz/classes/external.php | 44 ++++++++++++++++++--- mod/quiz/tests/external_test.php | 65 ++++++++++++++++++++++++++++++-- mod/quiz/upgrade.txt | 6 +++ 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index f7efb989a98..23d588716a0 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -877,6 +877,14 @@ class mod_quiz_external extends external_api { 'type' => new external_value(PARAM_ALPHANUMEXT, 'question type, i.e: multichoice'), 'page' => new external_value(PARAM_INT, 'page of the quiz this question appears on'), 'html' => new external_value(PARAM_RAW, 'the question rendered'), + 'responsefileareas' => new external_multiple_structure( + new external_single_structure( + array( + 'area' => new external_value(PARAM_NOTAGS, 'File area name'), + 'files' => new external_files('Response files for the question', VALUE_OPTIONAL), + ) + ), 'Response file areas including files', VALUE_OPTIONAL + ), 'sequencecheck' => new external_value(PARAM_INT, 'the number of real steps in this attempt', VALUE_OPTIONAL), 'lastactiontime' => new external_value(PARAM_INT, 'the timestamp of the most recent step in this question attempt', VALUE_OPTIONAL), @@ -914,23 +922,47 @@ class mod_quiz_external extends external_api { $contextid = $attemptobj->get_quizobj()->get_context()->id; $displayoptions = $attemptobj->get_display_options($review); $renderer = $PAGE->get_renderer('mod_quiz'); + $contextid = $attemptobj->get_quizobj()->get_context()->id; foreach ($attemptobj->get_slots($page) as $slot) { + $qtype = $attemptobj->get_question_type_name($slot); + $qattempt = $attemptobj->get_question_attempt($slot); + + // Get response files (for questions like essay that allows attachments). + $responsefileareas = []; + foreach (question_bank::get_qtype($qtype)->response_file_areas() as $area) { + if ($files = $attemptobj->get_question_attempt($slot)->get_last_qt_files($area, $contextid)) { + $responsefileareas[$area]['area'] = $area; + $responsefileareas[$area]['files'] = []; + + foreach ($files as $file) { + $responsefileareas[$area]['files'][] = array( + 'filename' => $file->get_filename(), + 'fileurl' => $qattempt->get_response_file_url($file), + 'filesize' => $file->get_filesize(), + 'filepath' => $file->get_filepath(), + 'mimetype' => $file->get_mimetype(), + 'timemodified' => $file->get_timemodified(), + ); + } + } + } $question = array( 'slot' => $slot, - 'type' => $attemptobj->get_question_type_name($slot), + 'type' => $qtype, 'page' => $attemptobj->get_question_page($slot), 'flagged' => $attemptobj->is_question_flagged($slot), 'html' => $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code(), - 'sequencecheck' => $attemptobj->get_question_attempt($slot)->get_sequence_check_count(), - 'lastactiontime' => $attemptobj->get_question_attempt($slot)->get_last_step()->get_timecreated(), - 'hasautosavedstep' => $attemptobj->get_question_attempt($slot)->has_autosaved_step() + 'responsefileareas' => $responsefileareas, + 'sequencecheck' => $qattempt->get_sequence_check_count(), + 'lastactiontime' => $qattempt->get_last_step()->get_timecreated(), + 'hasautosavedstep' => $qattempt->has_autosaved_step() ); if ($attemptobj->is_real_question($slot)) { $question['number'] = $attemptobj->get_question_number($slot); - $showcorrectness = $displayoptions->correctness && $attemptobj->get_question_attempt($slot)->has_marks(); + $showcorrectness = $displayoptions->correctness && $qattempt->has_marks(); if ($showcorrectness) { $question['state'] = (string) $attemptobj->get_question_state($slot); } @@ -938,7 +970,7 @@ class mod_quiz_external extends external_api { $question['blockedbyprevious'] = $attemptobj->is_blocked_by_previous_question($slot); } if ($displayoptions->marks >= question_display_options::MAX_ONLY) { - $question['maxmark'] = $attemptobj->get_question_attempt($slot)->get_max_mark(); + $question['maxmark'] = $qattempt->get_max_mark(); } if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) { $question['mark'] = $attemptobj->get_question_mark($slot); diff --git a/mod/quiz/tests/external_test.php b/mod/quiz/tests/external_test.php index 14dec66b12f..6de933da16f 100644 --- a/mod/quiz/tests/external_test.php +++ b/mod/quiz/tests/external_test.php @@ -109,9 +109,11 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { * @param boolean $startattempt whether to start a new attempt * @param boolean $finishattempt whether to finish the new attempt * @param string $behaviour the quiz preferredbehaviour, defaults to 'deferredfeedback'. + * @param boolean $includeqattachments whether to include a question that supports attachments, defaults to false. * @return array array containing the quiz, context and the attempt */ - private function create_quiz_with_questions($startattempt = false, $finishattempt = false, $behaviour = 'deferredfeedback') { + private function create_quiz_with_questions($startattempt = false, $finishattempt = false, $behaviour = 'deferredfeedback', + $includeqattachments = false) { // Create a new quiz with attempts. $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); @@ -130,6 +132,12 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); quiz_add_quiz_question($question->id, $quiz); + if ($includeqattachments) { + $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id, 'attachments' => 1, + 'attachmentsrequired' => 1)); + quiz_add_quiz_question($question->id, $quiz); + } + $quizobj = quiz::create($quiz->id, $this->student->id); // Set grade to pass. @@ -1126,8 +1134,9 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { global $DB; $timenow = time(); - // Create a new quiz with two questions and one attempt started. - list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true); + // Create a new quiz with three questions and one attempt started. + list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false, + 'deferredfeedback', true); // Response for slot 1. $prefix = $quba->get_field_prefix(1); @@ -1144,9 +1153,12 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']); + $result = mod_quiz_external::get_attempt_data($attempt->id, 2); + // Now, get the summary. $result = mod_quiz_external::get_attempt_summary($attempt->id); $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); + $this->assertDebuggingCalled(); // Expect $PAGE->set_url debugging. // Check it's marked as completed only the first one. $this->assertEquals('complete', $result['questions'][0]['state']); @@ -1182,12 +1194,57 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { $result = mod_quiz_external::get_attempt_summary($attempt->id); $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); - // Check it's marked as completed only the first one. + // Check it's marked as completed the two first questions. $this->assertEquals('complete', $result['questions'][0]['state']); $this->assertEquals('complete', $result['questions'][1]['state']); $this->assertFalse($result['questions'][0]['flagged']); $this->assertTrue($result['questions'][1]['flagged']); + // Add files in the attachment response. + $draftitemid = file_get_unused_draft_itemid(); + $filerecordinline = array( + 'contextid' => context_user::instance($this->student->id)->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $draftitemid, + 'filepath' => '/', + 'filename' => 'faketxt.txt', + ); + $fs = get_file_storage(); + $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.'); + + // Last slot. + $prefix = $quba->get_field_prefix(3); + $data = array( + array('name' => 'slots', 'value' => 3), + array('name' => $prefix . ':sequencecheck', + 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), + array('name' => $prefix . 'answer', 'value' => 'Some test'), + array('name' => $prefix . 'answerformat', 'value' => FORMAT_HTML), + array('name' => $prefix . 'attachments', 'value' => $draftitemid), + ); + + $result = mod_quiz_external::process_attempt($attempt->id, $data); + $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); + $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']); + + // Now, get the summary. + $result = mod_quiz_external::get_attempt_summary($attempt->id); + $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); + + $this->assertEquals('complete', $result['questions'][0]['state']); + $this->assertEquals('complete', $result['questions'][1]['state']); + $this->assertEquals('complete', $result['questions'][2]['state']); + $this->assertFalse($result['questions'][0]['flagged']); + $this->assertTrue($result['questions'][1]['flagged']); + $this->assertFalse($result['questions'][2]['flagged']); + + // Check submitted files are there. + $this->assertCount(1, $result['questions'][2]['responsefileareas']); + $this->assertEquals('attachments', $result['questions'][2]['responsefileareas'][0]['area']); + $this->assertCount(1, $result['questions'][2]['responsefileareas'][0]['files']); + $this->assertEquals($filerecordinline['filename'], $result['questions'][2]['responsefileareas'][0]['files'][0]['filename']); + // Finish the attempt. $sink = $this->redirectMessages(); $result = mod_quiz_external::process_attempt($attempt->id, array(), true); diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 929452baad0..d3f6c09585d 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -1,5 +1,11 @@ This files describes API changes in the quiz code. +=== 3.10 === + +* External functions mod_quiz_external::get_attempt_data, mod_quiz_external::get_attempt_summary + and mod_quiz_external::get_attempt_review now return a new additional optional field: + - responsefileareas: Containing the user responses to questions file area names including files. + === 3.7 === * Quiz_cron() has been removed. Sub-plugins should implemented scheduled tasks, however legacy cron in subplugins are