MDL-39980 question engine: Attempt on last loses response files

When doing Each attempt builds on last, we need to copy any response
files into a draft file area, and then re-save them.

While writing the unit test for this, I had to deal with a todo in the
question engine so that questions with files in the response could be
unit-tested.

I also found an fixed a bug with qtype_essay_question::is_same_response
and fixed some notices in the existing essay/manual graded unit tests.
This commit is contained in:
Tim Hunt 2013-08-05 16:51:05 +01:00
parent 838d78a9ff
commit afb1b3d03b
8 changed files with 221 additions and 18 deletions

View file

@ -38,7 +38,7 @@ require_once(dirname(__FILE__) . '/../../../engine/tests/helpers.php');
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_manualgraded_walkthrough_test extends qbehaviour_walkthrough_test_base {
class qbehaviour_manualgraded_walkthrough_testcase extends qbehaviour_walkthrough_test_base {
public function test_manual_graded_essay() {
// Create an essay question.
@ -56,7 +56,7 @@ class qbehaviour_manualgraded_walkthrough_test extends qbehaviour_walkthrough_te
$this->get_does_not_contain_feedback_expectation());
// Simulate some data submitted by the student.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_PLAIN));
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
@ -68,16 +68,16 @@ class qbehaviour_manualgraded_walkthrough_test extends qbehaviour_walkthrough_te
// Process the same data again, check it does not create a new step.
$numsteps = $this->get_step_count();
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_PLAIN));
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
$this->check_step_count($numsteps);
// Process different data, check it creates a new step.
$this->process_submission(array('answer' => ''));
$this->process_submission(array('answer' => '', 'answerformat' => FORMAT_HTML));
$this->check_step_count($numsteps + 1);
$this->check_current_state(question_state::$todo);
// Change back, check it creates a new step.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_PLAIN));
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
$this->check_step_count($numsteps + 2);
// Finish the attempt.
@ -206,7 +206,7 @@ class qbehaviour_manualgraded_walkthrough_test extends qbehaviour_walkthrough_te
$this->check_current_mark(null);
// Simulate some data submitted by the student.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_PLAIN));
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
@ -283,7 +283,7 @@ class qbehaviour_manualgraded_walkthrough_test extends qbehaviour_walkthrough_te
$this->get_does_not_contain_feedback_expectation());
// Simulate some data submitted by the student.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_PLAIN));
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);

View file

@ -1379,6 +1379,35 @@ class question_file_loader implements question_response_files {
public function get_files() {
return $this->step->get_qt_files($this->name, $this->contextid);
}
/**
* Copy these files into a draft area, and return the corresponding
* {@link question_file_saver} that can save them again.
*
* This is used by {@link question_attempt::start_based_on()}, which is used
* (for example) by the quizzes 'Each attempt builds on last' feature.
*
* @return question_file_saver that can re-save these files again.
*/
public function get_question_file_saver() {
// Value will be either a plain MD5 hash, or some real content, followed
// by an MD5 hash in a HTML comment. We only want the value in the latter case.
if (preg_match('/\s*<!-- File hash: [0-9a-zA-Z]{32} -->\s*$/', $this->value)) {
$value = preg_replace('/\s*<!-- File hash: [0-9a-zA-Z]{32} -->\s*$/', '', $this->value);
} else if (preg_match('/^[0-9a-zA-Z]{32}$/', $this->value)) {
$value = null;
} else {
throw new coding_exception('$value passed to question_file_loader::get_question_file_saver' .
' was not of the expected form.');
}
list($draftid, $text) = $this->step->prepare_response_files_draft_itemid_with_text(
$this->name, $this->contextid, $value);
return new question_file_saver($draftid, 'question', 'response_' . $this->name, $text);
}
}

View file

@ -923,7 +923,13 @@ class question_attempt {
* @return array name => value pairs.
*/
protected function get_resume_data() {
return $this->behaviour->get_resume_data();
$resumedata = $this->behaviour->get_resume_data();
foreach ($resumedata as $name => $value) {
if ($value instanceof question_file_loader) {
$resumedata[$name] = $value->get_question_file_saver();
}
}
return $resumedata;
}
/**
@ -975,11 +981,12 @@ class question_attempt {
*/
protected function process_response_files($name, $draftidname, $postdata = null, $text = null) {
if ($postdata) {
// There can be no files with test data (at the moment).
return null;
// For simulated posts, get the draft itemid from there.
$draftitemid = $this->get_submitted_var($draftidname, PARAM_INT, $postdata);
} else {
$draftitemid = file_get_submitted_draft_itemid($draftidname);
}
$draftitemid = file_get_submitted_draft_itemid($draftidname);
if (!$draftitemid) {
return null;
}

View file

@ -106,7 +106,7 @@ class question_attempt_step {
global $USER;
if (!is_array($data)) {
echo format_backtrace(debug_backtrace());
throw new coding_exception('$data must be an array when constructing a question_attempt_step.');
}
$this->state = question_state::$unprocessed;
$this->data = $data;

View file

@ -87,12 +87,12 @@ class qtype_essay_question extends question_with_responses {
public function is_same_response(array $prevresponse, array $newresponse) {
if (array_key_exists('answer', $prevresponse) && $prevresponse['answer'] !== $this->responsetemplate) {
$value1 = $prevresponse['answer'];
$value1 = (string) $prevresponse['answer'];
} else {
$value1 = '';
}
if (array_key_exists('answer', $newresponse) && $newresponse['answer'] !== $this->responsetemplate) {
$value2 = $newresponse['answer'];
$value2 = (string) $newresponse['answer'];
} else {
$value2 = '';
}

View file

@ -78,6 +78,29 @@ class qtype_essay_test_helper extends question_test_helper {
return $q;
}
/**
* Make the data what would be received from the editing form for an essay
* question using the HTML editor allowing embedded files as input, and up
* to three attachments.
*
* @return stdClass the data that would be returned by $form->get_gata();
*/
public function get_essay_question_form_data_editorfilepicker() {
$fromform = new stdClass();
$fromform->name = 'Essay question with filepicker and attachments';
$fromform->questiontext = array('text' => 'Please write a story about a frog.', 'format' => FORMAT_HTML);
$fromform->defaultmark = 1.0;
$fromform->generalfeedback = array('text' => 'I hope your story had a beginning, a middle and an end.', 'format' => FORMAT_HTML);
$fromform->responseformat = 'editorfilepicker';
$fromform->responsefieldlines = 10;
$fromform->attachments = 3;
$fromform->graderinfo = array('text' => '', 'format' => FORMAT_HTML);
$fromform->responsetemplate = array('text' => '', 'format' => FORMAT_HTML);
return $fromform;
}
/**
* Makes an essay question using plain text input.
* @return qtype_essay_question

View file

@ -36,7 +36,7 @@ require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_essay_question_test extends advanced_testcase {
class qtype_essay_question_testcase extends advanced_testcase {
public function test_get_question_summary() {
$essay = test_question_maker::make_an_essay_question();
$essay->questiontext = 'Hello <img src="http://example.com/globe.png" alt="world" />';
@ -46,8 +46,8 @@ class qtype_essay_question_test extends advanced_testcase {
public function test_summarise_response() {
$longstring = str_repeat('0123456789', 50);
$essay = test_question_maker::make_an_essay_question();
$this->assertEquals($longstring,
$essay->summarise_response(array('answer' => $longstring, 'answerformat' => FORMAT_PLAIN)));
$this->assertEquals($longstring, $essay->summarise_response(
array('answer' => $longstring, 'answerformat' => FORMAT_HTML)));
}
public function test_is_same_response() {

View file

@ -35,7 +35,7 @@ require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_essay_walkthrough_test extends qbehaviour_walkthrough_test_base {
class qtype_essay_walkthrough_testcase extends qbehaviour_walkthrough_test_base {
protected function check_contains_textarea($name, $content = '', $height = 10) {
$fieldname = $this->quba->get_field_prefix($this->slot) . $name;
@ -50,6 +50,28 @@ class qtype_essay_walkthrough_test extends qbehaviour_walkthrough_test_base {
}
}
/**
* Helper method: Store a test file with a given name and contents in a
* draft file area.
*
* @param int $usercontextid user context id.
* @param int $draftitemid draft item id.
* @param string $filename filename.
* @param string $contents file contents.
*/
protected function save_file_to_draft_area($usercontextid, $draftitemid, $filename, $contents) {
$fs = get_file_storage();
$filerecord = new stdClass();
$filerecord->contextid = $usercontextid;
$filerecord->component = 'user';
$filerecord->filearea = 'draft';
$filerecord->itemid = $draftitemid;
$filerecord->filepath = '/';
$filerecord->filename = $filename;
$fs->create_file_from_string($filerecord, $contents);
}
public function test_deferred_feedback_html_editor() {
// Create an essay question.
@ -204,4 +226,126 @@ class qtype_essay_walkthrough_test extends qbehaviour_walkthrough_test_base {
$this->get_contains_question_text_expectation($q),
$this->get_contains_general_feedback_expectation($q));
}
public function test_deferred_feedback_html_editor_with_files_attempt_on_last() {
global $CFG, $USER;
$this->resetAfterTest(true);
$this->setAdminUser();
$usercontextid = context_user::instance($USER->id)->id;
$fs = get_file_storage();
// Create an essay question in the DB.
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $generator->create_question_category();
$question = $generator->create_question('essay', 'editorfilepicker', array('category' => $cat->id));
// Start attempt at the question.
$q = question_bank::load_question($question->id);
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_step_count(1);
// Process a response and check the expected result.
// First we need to get the draft item ids.
$this->render();
if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
throw new coding_exception('Editor draft item id not found.');
}
$editordraftid = $matches[1];
if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
throw new coding_exception('File manager draft item id not found.');
}
$attachementsdraftid = $matches[1];
$this->save_file_to_draft_area($usercontextid, $editordraftid, 'smile.txt', ':-)');
$this->save_file_to_draft_area($usercontextid, $attachementsdraftid, 'greeting.txt', 'Hello world!');
$this->process_submission(array(
'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
"/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
'" alt="smile">.',
'answerformat' => FORMAT_HTML,
'answer:itemid' => $editordraftid,
'attachments' => $attachementsdraftid));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
$this->save_quba();
// Save the same response again, and verify no new step is created.
$this->load_quba();
$this->render();
if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
throw new coding_exception('Editor draft item id not found.');
}
$editordraftid = $matches[1];
if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
throw new coding_exception('File manager draft item id not found.');
}
$attachementsdraftid = $matches[1];
$this->process_submission(array(
'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
"/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
'" alt="smile">.',
'answerformat' => FORMAT_HTML,
'answer:itemid' => $editordraftid,
'attachments' => $attachementsdraftid));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(2);
// Now submit all and finish.
$this->finish();
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->save_quba();
// Now start a new attempt based on the old one.
$this->load_quba();
$oldqa = $this->get_question_attempt();
$q = question_bank::load_question($question->id);
$this->quba = question_engine::make_questions_usage_by_activity('unit_test',
context_system::instance());
$this->quba->set_preferred_behaviour('deferredfeedback');
$this->slot = $this->quba->add_question($q, 1);
$this->quba->start_question_based_on($this->slot, $oldqa);
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(1);
$this->save_quba();
// Now save the same response again, and ensure that a new step is not created.
$this->load_quba();
$this->render();
if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
throw new coding_exception('Editor draft item id not found.');
}
$editordraftid = $matches[1];
if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
throw new coding_exception('File manager draft item id not found.');
}
$attachementsdraftid = $matches[1];
$this->process_submission(array(
'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
"/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
'" alt="smile">.',
'answerformat' => FORMAT_HTML,
'answer:itemid' => $editordraftid,
'attachments' => $attachementsdraftid));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_step_count(1);
}
}