MDL-40992 question engine: new ways modify question usages

* A method to change the max mark for one question_attempt in the usage

* A method to replace one question in a usage with another, moving the
old question_attempt to the end.

* Methods to set and get metadata (string name value pairs) for each
question_attempt in the usage. This gets stored in the first step in a
way that should not interfere with anything else.
This commit is contained in:
Tim Hunt 2015-03-13 19:07:44 +00:00
parent 47be39ef41
commit f6579bea94
8 changed files with 610 additions and 87 deletions

View File

@ -252,6 +252,48 @@ class question_engine_data_mapper {
return $this->prepare_step_data($step, $record->id, $context);
}
/**
* Store new metadata for an existing {@link question_attempt} in the database.
*
* Private method, only for use by other parts of the question engine.
*
* @param question_attempt $qa the question attempt to store meta data for.
* @param array $names the names of the metadata variables to store.
* @return array of question_attempt_step_data rows, that still need to be inserted.
*/
public function insert_question_attempt_metadata(question_attempt $qa, array $names) {
$firststep = $qa->get_step(0);
$rows = array();
foreach ($names as $name) {
$data = new stdClass();
$data->attemptstepid = $firststep->get_id();
$data->name = ':_' . $name;
$data->value = $firststep->get_metadata_var($name);
$rows[] = $data;
}
return $rows;
}
/**
* Updates existing metadata for an existing {@link question_attempt} in the database.
*
* Private method, only for use by other parts of the question engine.
*
* @param question_attempt $qa the question attempt to store meta data for.
* @param array $names the names of the metadata variables to store.
* @return array of question_attempt_step_data rows, that still need to be inserted.
*/
public function update_question_attempt_metadata(question_attempt $qa, array $names) {
global $DB;
list($condition, $params) = $DB->get_in_or_equal($names);
$params[] = $qa->get_step(0)->get_id();
$DB->delete_records_select('question_attempt_step_data',
'name ' . $condition . ' AND attemptstepid = ?', $params);
return $this->insert_question_attempt_metadata($qa, $names);
}
/**
* Load a {@link question_attempt_step} from the database.
*
@ -867,6 +909,7 @@ ORDER BY
public function update_question_attempt(question_attempt $qa) {
$record = new stdClass();
$record->id = $qa->get_database_id();
$record->slot = $qa->get_slot();
$record->variant = $qa->get_variant();
$record->maxmark = $qa->get_max_mark();
$record->minfraction = $qa->get_min_fraction();
@ -880,16 +923,6 @@ ORDER BY
$this->db->update_record('question_attempts', $record);
}
/**
* Delete a question_attempts row to reflect any changes in a question_attempt
* (but not any of its steps).
* @param question_attempt $qa the question attempt that has been deleted.
*/
public function delete_question_attempt(question_attempt $qa) {
$conditions = array('questionusageid' => $qa->get_usage_id(), 'slot' => $qa->get_slot());
$this->db->delete_records('question_attempts', $conditions);
}
/**
* Delete a question_usage_by_activity and all its associated
*
@ -1254,24 +1287,18 @@ class question_engine_unit_of_work implements question_usage_observer {
/** @var boolean whether any of the fields of the usage have been changed. */
protected $modified = false;
/**
* @var array list of slot => {@link question_attempt}s that
* were already in the usage, and which have been modified.
*/
protected $attemptsmodified = array();
/**
* @var array list of slot => {@link question_attempt}s that
* were already in the usage, and which have been deleted.
*/
protected $attemptsdeleted = array();
/**
* @var array list of slot => {@link question_attempt}s that
* have been added to the usage.
*/
protected $attemptsadded = array();
/**
* @var array list of slot => {@link question_attempt}s that
* were already in the usage, and which have been modified.
*/
protected $attemptsmodified = array();
/**
* @var array of array(question_attempt_step, question_attempt id, seq number)
* of steps that have been added to question attempts in this usage.
@ -1290,6 +1317,16 @@ class question_engine_unit_of_work implements question_usage_observer {
*/
protected $stepsdeleted = array();
/**
* @var array int slot => string name => question_attempt.
*/
protected $metadataadded = array();
/**
* @var array int slot => string name => question_attempt.
*/
protected $metadatamodified = array();
/**
* Constructor.
* @param question_usage_by_activity $quba the usage to track.
@ -1302,6 +1339,10 @@ class question_engine_unit_of_work implements question_usage_observer {
$this->modified = true;
}
public function notify_attempt_added(question_attempt $qa) {
$this->attemptsadded[$qa->get_slot()] = $qa;
}
public function notify_attempt_modified(question_attempt $qa) {
$slot = $qa->get_slot();
if (!array_key_exists($slot, $this->attemptsadded)) {
@ -1309,27 +1350,27 @@ class question_engine_unit_of_work implements question_usage_observer {
}
}
/**
* Notify when attempt deleted
*
* @see question_usage_observer::notify_attempt_deleted()
*/
public function notify_attempt_deleted(question_attempt $qa) {
$slot = $qa->get_slot();
if (!array_key_exists($slot, $this->attemptsadded)) {
$this->attemptsdeleted[$slot] = $qa;
}
}
public function notify_attempt_moved(question_attempt $qa, $oldslot) {
$newslot = $qa->get_slot();
/**
* Notify when attempt added
*
* @see question_usage_observer::notify_attempt_added()
*/
public function notify_attempt_added(question_attempt $qa) {
$slot = $qa->get_slot();
if (!array_key_exists($slot, $this->attemptsadded)) {
$this->attemptsadded[$slot] = $qa;
if (array_key_exists($oldslot, $this->attemptsadded)) {
unset($this->attemptsadded[$oldslot]);
$this->attemptsadded[$newslot] = $qa;
return;
}
if (array_key_exists($oldslot, $this->attemptsmodified)) {
unset($this->attemptsmodified[$oldslot]);
}
$this->attemptsmodified[$newslot] = $qa;
if (array_key_exists($oldslot, $this->metadataadded)) {
$this->metadataadded[$newslot] = $this->metadataadded[$oldslot];
unset($this->metadataadded[$oldslot]);
}
if (array_key_exists($oldslot, $this->metadatamodified)) {
$this->metadatamodified[$newslot] = $this->metadatamodified[$oldslot];
unset($this->metadatamodified[$oldslot]);
}
}
@ -1407,6 +1448,42 @@ class question_engine_unit_of_work implements question_usage_observer {
$this->stepsdeleted[$stepid] = $step;
}
public function notify_metadata_added(question_attempt $qa, $name) {
if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
return;
}
if ($this->is_step_added($qa->get_step(0)) !== false) {
return;
}
if (isset($this->metadataadded[$qa->get_slot()][$name])) {
return;
}
$this->metadataadded[$qa->get_slot()][$name] = $qa;
}
public function notify_metadata_modified(question_attempt $qa, $name) {
if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
return;
}
if ($this->is_step_added($qa->get_step(0)) !== false) {
return;
}
if (isset($this->metadataadded[$qa->get_slot()][$name])) {
return;
}
if (isset($this->metadatamodified[$qa->get_slot()][$name])) {
return;
}
$this->metadatamodified[$qa->get_slot()][$name] = $qa;
}
/**
* @param question_attempt_step $step a step
* @return int|false if the step is in the list of steps to be added, return
@ -1473,8 +1550,8 @@ class question_engine_unit_of_work implements question_usage_observer {
$step, $questionattemptid, $seq, $this->quba->get_owning_context());
}
foreach ($this->attemptsdeleted as $qa) {
$dm->delete_question_attempt($qa);
foreach ($this->attemptsmodified as $qa) {
$dm->update_question_attempt($qa);
}
foreach ($this->attemptsadded as $qa) {
@ -1482,18 +1559,31 @@ class question_engine_unit_of_work implements question_usage_observer {
$qa, $this->quba->get_owning_context());
}
foreach ($this->attemptsmodified as $qa) {
$dm->update_question_attempt($qa);
foreach ($this->metadataadded as $info) {
$qa = reset($info);
$stepdata[] = $dm->insert_question_attempt_metadata($qa, array_keys($info));
}
foreach ($this->metadatamodified as $info) {
$qa = reset($info);
$stepdata[] = $dm->update_question_attempt_metadata($qa, array_keys($info));
}
if ($this->modified) {
$dm->update_questions_usage_by_activity($this->quba);
}
if (!$stepdata) {
return;
if ($stepdata) {
$dm->insert_all_step_data(call_user_func_array('array_merge', $stepdata));
}
$dm->insert_all_step_data(call_user_func_array('array_merge', $stepdata));
$this->stepsdeleted = array();
$this->stepsmodified = array();
$this->stepsadded = array();
$this->attemptsdeleted = array();
$this->attemptsadded = array();
$this->attemptsmodified = array();
$this->modified = false;
}
}

View File

@ -362,7 +362,7 @@ class question_attempt {
/**
* Get one of the steps in this attempt.
*
* @param int $i the step number.
* @param int $i the step number, which counts from 0.
* @return question_attempt_step
*/
public function get_step($i) {
@ -748,6 +748,30 @@ class question_attempt {
return $this->behaviour->summarise_action($step);
}
/**
* Return one of the bits of metadata for a this question attempt.
* @param string $name the name of the metadata variable to return.
* @return string the value of that metadata variable.
*/
public function get_metadata($name) {
return $this->get_step(0)->get_metadata_var($name);
}
/**
* Set some metadata for this question attempt.
* @param string $name the name of the metadata variable to return.
* @param string $value the value to set that metadata variable to.
*/
public function set_metadata($name, $value) {
$firststep = $this->get_step(0);
if (!$firststep->has_metadata_var($name)) {
$this->observer->notify_metadata_added($this, $name);
} else if ($value !== $firststep->get_metadata_var($name)) {
$this->observer->notify_metadata_modified($this, $name);
}
$firststep->set_metadata_var($name, $value);
}
/**
* Helper function used by {@link rewrite_pluginfile_urls()} and
* {@link rewrite_response_pluginfile_urls()}.
@ -931,6 +955,10 @@ class question_attempt {
public function start($preferredbehaviour, $variant, $submitteddata = array(),
$timestamp = null, $userid = null, $existingstepid = null) {
if ($this->get_num_steps() > 0) {
throw new coding_exception('Cannot start a question that is already started.');
}
// Initialise the behaviour.
$this->variant = $variant;
if (is_string($preferredbehaviour)) {
@ -1266,6 +1294,15 @@ class question_attempt {
}
}
/**
* Change the max mark for this question_attempt.
* @param float $maxmark the new max mark.
*/
public function set_max_mark($maxmark) {
$this->maxmark = $maxmark;
$this->observer->notify_attempt_modified($this);
}
/**
* Perform a manual grading action on this attempt.
* @param string $comment the comment being added.

View File

@ -370,6 +370,48 @@ class question_attempt_step {
return $this->data;
}
/**
* Set a metadata variable.
*
* Do not call this method directly from your code. It is for internal
* use only. You should call {@link question_usage::set_question_attempt_metadata()}.
*
* @param string $name the name of the variable to set. [a-z][a-z0-9]*.
* @param string $value the value to set.
*/
public function set_metadata_var($name, $value) {
$this->data[':_' . $name] = $value;
}
/**
* Whether this step has a metadata variable.
*
* Do not call this method directly from your code. It is for internal
* use only. You should call {@link question_usage::get_question_attempt_metadata()}.
*
* @param string $name the name of the variable to set. [a-z][a-z0-9]*.
* @return bool the value to set previously, or null if this variable was never set.
*/
public function has_metadata_var($name) {
return isset($this->data[':_' . $name]);
}
/**
* Get a metadata variable.
*
* Do not call this method directly from your code. It is for internal
* use only. You should call {@link question_usage::get_question_attempt_metadata()}.
*
* @param string $name the name of the variable to set. [a-z][a-z0-9]*.
* @return string the value to set previously, or null if this variable was never set.
*/
public function get_metadata_var($name) {
if (!$this->has_metadata_var($name)) {
return null;
}
return $this->data[':_' . $name];
}
/**
* Create a question_attempt_step from records loaded from the database.
* @param Iterator $records Raw records loaded from the database.

View File

@ -172,6 +172,42 @@ class question_usage_by_activity {
return $qa->get_slot();
}
/**
* Add another question to this usage, in the place of an existing slot.
* The question_attempt that was in that slot is moved to the end at a new
* slot number, which is returned.
*
* The added question is not started until you call {@link start_question()}
* on it.
*
* @param int $slot the slot-number of the question to replace.
* @param question_definition $question the question to add.
* @param number $maxmark the maximum this question will be marked out of in
* this attempt (optional). If not given, the max mark from the $qa we
* are replacing is used.
* @return int the new slot number of the question that was displaced.
*/
public function add_question_in_place_of_other($slot, question_definition $question, $maxmark = null) {
$newslot = $this->next_slot_number();
$oldqa = $this->get_question_attempt($slot);
$oldqa->set_slot($newslot);
$this->questionattempts[$newslot] = $oldqa;
if ($maxmark === null) {
$maxmark = $oldqa->get_max_mark();
}
$qa = new question_attempt($question, $this->get_id(), $this->observer, $maxmark);
$qa->set_slot($slot);
$this->questionattempts[$slot] = $qa;
$this->observer->notify_attempt_moved($oldqa, $slot);
$this->observer->notify_attempt_added($qa);
return $newslot;
}
/**
* The slot number that will be allotted to the next question added.
*/
@ -377,6 +413,27 @@ class question_usage_by_activity {
return $this->get_question_attempt($slot)->get_right_answer_summary();
}
/**
* Return one of the bits of metadata for a particular question attempt in
* this usage.
* @param int $slot the slot number of the question of inereest.
* @param string $name the name of the metadata variable to return.
* @return string the value of that metadata variable.
*/
public function get_question_attempt_metadata($slot, $name) {
return $this->get_question_attempt($slot)->get_metadata($name);
}
/**
* Set some metadata for a particular question attempt in this usage.
* @param int $slot the slot number of the question of inerest.
* @param string $name the name of the metadata variable to return.
* @param string $value the value to set that metadata variable to.
*/
public function set_question_attempt_metadata($slot, $name, $value) {
$this->get_question_attempt($slot)->set_metadata($name, $value);
}
/**
* Get the {@link core_question_renderer}, in collaboration with appropriate
* {@link qbehaviour_renderer} and {@link qtype_renderer} subclasses, to generate the
@ -822,22 +879,6 @@ class question_usage_by_activity {
$this->observer->notify_attempt_modified($newqa);
}
/**
* Replace a question in this usage.
* @param int $slot the number used to identify this question within this usage.*
*/
public function replace_question($slot) {
global $OUTPUT;
$oldqa = $this->get_question_attempt($slot);
$newqa = new question_attempt($oldqa->get_question(), $oldqa->get_usage_id(), $this->observer);
$newqa->set_database_id($oldqa->get_database_id());
$newqa->set_slot($oldqa->get_slot());
$this->questionattempts[$slot] = $newqa;
$this->observer->notify_attempt_deleted($oldqa);
$this->observer->notify_attempt_added($newqa);
$this->start_question($slot);
}
/**
* Regrade all the questions in this usage (without changing their max mark).
* @param bool $finished whether each question should be forced to be finished
@ -849,6 +890,15 @@ class question_usage_by_activity {
}
}
/**
* Change the max mark for this question_attempt.
* @param int $slot the slot number of the question of inerest.
* @param float $maxmark the new max mark.
*/
public function set_max_mark($slot, $maxmark) {
$this->get_question_attempt($slot)->set_max_mark($maxmark);
}
/**
* Create a question_usage_by_activity from records loaded from the database.
*
@ -983,12 +1033,6 @@ interface question_usage_observer {
/** Called when a field of the question_usage_by_activity is changed. */
public function notify_modified();
/**
* Called when the fields of a question attempt in this usage are modified.
* @param question_attempt $qa the newly added question attempt.
*/
public function notify_attempt_modified(question_attempt $qa);
/**
* Called when a new question attempt is added to this usage.
* @param question_attempt $qa the newly added question attempt.
@ -996,10 +1040,17 @@ interface question_usage_observer {
public function notify_attempt_added(question_attempt $qa);
/**
* Called when the fields of a question attempt in this usage are deleted.
* @param question_attempt $qa
* Called when the fields of a question attempt in this usage are modified.
* @param question_attempt $qa the newly added question attempt.
*/
public function notify_attempt_deleted(question_attempt $qa);
public function notify_attempt_modified(question_attempt $qa);
/**
* Called when a question_attempt has been moved to a new slot.
* @param question_attempt $qa The question attempt that was moved.
* @param int $oldslot The previous slot number of that attempt.
*/
public function notify_attempt_moved(question_attempt $qa, $oldslot);
/**
* Called when a new step is added to a question attempt in this usage.
@ -1024,6 +1075,19 @@ interface question_usage_observer {
*/
public function notify_step_deleted(question_attempt_step $step, question_attempt $qa);
/**
* Called when a new metadata variable is set on a question attempt in this usage.
* @param question_attempt $qa the question attempt the metadata is being added to.
* @param int $name the name of the metadata variable added.
*/
public function notify_metadata_added(question_attempt $qa, $name);
/**
* Called when a metadata variable on a question attempt in this usage is updated.
* @param question_attempt $qa the question attempt where the metadata is being modified.
* @param int $name the name of the metadata variable modified.
*/
public function notify_metadata_modified(question_attempt $qa, $name);
}
@ -1037,11 +1101,11 @@ interface question_usage_observer {
class question_usage_null_observer implements question_usage_observer {
public function notify_modified() {
}
public function notify_attempt_added(question_attempt $qa) {
}
public function notify_attempt_modified(question_attempt $qa) {
}
public function notify_attempt_deleted(question_attempt $qa) {
}
public function notify_attempt_added(question_attempt $qa) {
public function notify_attempt_moved(question_attempt $qa, $oldslot) {
}
public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
}
@ -1049,4 +1113,8 @@ class question_usage_null_observer implements question_usage_observer {
}
public function notify_step_deleted(question_attempt_step $step, question_attempt $qa) {
}
public function notify_metadata_added(question_attempt $qa, $name) {
}
public function notify_metadata_modified(question_attempt $qa, $name) {
}
}

View File

@ -84,6 +84,14 @@ class testable_question_engine_unit_of_work extends question_engine_unit_of_work
public function get_steps_deleted() {
return $this->stepsdeleted;
}
public function get_metadata_added() {
return $this->metadataadded;
}
public function get_metadata_modified() {
return $this->metadatamodified;
}
}

View File

@ -91,8 +91,8 @@ class question_engine_unit_of_work_test extends data_loading_method_test_base {
array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo', null, 1256233720, 1, '-submit', 1),
array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo', null, 1256233720, 1, '-_triesleft', 1),
array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 3, 2, 'todo', null, 1256233740, 1, '-tryagain', 1),
array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 5, 3, 'gradedright', null, 1256233790, 1, 'answer', 'frog'),
array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 5, 3, 'gradedright', 1.0000000, 1256233790, 1, '-submit', 1),
array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 5, 3, 'gradedright', 0.6666667, 1256233790, 1, 'answer', 'frog'),
array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 5, 3, 'gradedright', 0.6666667, 1256233790, 1, '-submit', 1),
);
}
@ -103,6 +103,8 @@ class question_engine_unit_of_work_test extends data_loading_method_test_base {
$this->assertEquals(0, count($this->observer->get_steps_added()));
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_update_usage() {
@ -120,6 +122,9 @@ class question_engine_unit_of_work_test extends data_loading_method_test_base {
$this->assertEquals(1, count($newattempts));
$this->assertTrue($this->quba->get_question_attempt($slot) === reset($newattempts));
$this->assertSame($slot, key($newattempts));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_add_and_start_question() {
@ -136,6 +141,9 @@ class question_engine_unit_of_work_test extends data_loading_method_test_base {
$this->assertTrue($this->quba->get_question_attempt($slot) === reset($newattempts));
$this->assertSame($slot, key($newattempts));
$this->assertEquals(0, count($this->observer->get_steps_added()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_process_action() {
@ -157,6 +165,9 @@ class question_engine_unit_of_work_test extends data_loading_method_test_base {
list($newstep, $qaid, $seq) = reset($newsteps);
$this->assertSame($this->quba->get_question_attempt($this->slot)->get_last_step(), $newstep);
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_regrade_same_steps() {
@ -184,6 +195,9 @@ class question_engine_unit_of_work_test extends data_loading_method_test_base {
$this->assertSame(array($step, $updatedattempt->get_database_id(), $seq),
$updatedsteps[$seq]);
}
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_regrade_losing_steps() {
@ -220,6 +234,9 @@ class question_engine_unit_of_work_test extends data_loading_method_test_base {
$seconddeletedstep = end($deletedsteps);
$this->assertEquals(array('answer' => 'frog', '-submit' => 1),
$seconddeletedstep->get_all_data());
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_tricky_regrade() {
@ -258,5 +275,237 @@ class question_engine_unit_of_work_test extends data_loading_method_test_base {
}
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_move_question() {
$q = test_question_maker::make_question('truefalse');
$newslot = $this->quba->add_question_in_place_of_other($this->slot, $q);
$this->quba->start_question($this->slot);
$addedattempts = $this->observer->get_attempts_added();
$this->assertEquals(1, count($addedattempts));
$addedattempt = reset($addedattempts);
$this->assertSame($this->quba->get_question_attempt($this->slot), $addedattempt);
$updatedattempts = $this->observer->get_attempts_modified();
$this->assertEquals(1, count($updatedattempts));
$updatedattempt = reset($updatedattempts);
$this->assertSame($this->quba->get_question_attempt($newslot), $updatedattempt);
$this->assertEquals(0, count($this->observer->get_steps_added()));
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_move_question_then_modify() {
$q = test_question_maker::make_question('truefalse');
$newslot = $this->quba->add_question_in_place_of_other($this->slot, $q);
$this->quba->start_question($this->slot);
$this->quba->process_action($this->slot, array('answer' => 'frog', '-submit' => 1));
$this->quba->manual_grade($newslot, 'Test', 0.5, FORMAT_HTML);
$addedattempts = $this->observer->get_attempts_added();
$this->assertEquals(1, count($addedattempts));
$addedattempt = reset($addedattempts);
$this->assertSame($this->quba->get_question_attempt($this->slot), $addedattempt);
$updatedattempts = $this->observer->get_attempts_modified();
$this->assertEquals(1, count($updatedattempts));
$updatedattempt = reset($updatedattempts);
$this->assertSame($this->quba->get_question_attempt($newslot), $updatedattempt);
$newsteps = $this->observer->get_steps_added();
$this->assertEquals(1, count($newsteps));
list($newstep, $qaid, $seq) = reset($newsteps);
$this->assertSame($this->quba->get_question_attempt($newslot)->get_last_step(), $newstep);
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_move_question_then_move_again() {
$originalqa = $this->quba->get_question_attempt($this->slot);
$q1 = test_question_maker::make_question('truefalse');
$newslot = $this->quba->add_question_in_place_of_other($this->slot, $q1);
$this->quba->start_question($this->slot);
$q2 = test_question_maker::make_question('truefalse');
$newslot2 = $this->quba->add_question_in_place_of_other($newslot, $q2);
$this->quba->start_question($newslot);
$addedattempts = $this->observer->get_attempts_added();
$this->assertEquals(2, count($addedattempts));
$updatedattempts = $this->observer->get_attempts_modified();
$this->assertEquals(1, count($updatedattempts));
$updatedattempt = reset($updatedattempts);
$this->assertSame($originalqa, $updatedattempt);
$this->assertEquals(0, count($this->observer->get_steps_added()));
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_set_max_mark() {
$this->quba->set_max_mark($this->slot, 6.0);
$this->assertEquals(4.0, $this->quba->get_total_mark(), '', 0.0000005);
$this->assertEquals(0, count($this->observer->get_attempts_added()));
$updatedattempts = $this->observer->get_attempts_modified();
$this->assertEquals(1, count($updatedattempts));
$updatedattempt = reset($updatedattempts);
$this->assertSame($this->quba->get_question_attempt($this->slot), $updatedattempt);
$this->assertEquals(0, count($this->observer->get_steps_added()));
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_set_question_attempt_metadata() {
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'a value');
$this->assertEquals('a value', $this->quba->get_question_attempt_metadata($this->slot, 'metathingy'));
$this->assertEquals(0, count($this->observer->get_attempts_added()));
$this->assertEquals(0, count($this->observer->get_attempts_modified()));
$this->assertEquals(0, count($this->observer->get_steps_added()));
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(array($this->slot => array('metathingy' => $this->quba->get_question_attempt($this->slot))),
$this->observer->get_metadata_added());
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_set_question_attempt_metadata_then_change() {
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'a value');
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'different value');
$this->assertEquals('different value', $this->quba->get_question_attempt_metadata($this->slot, 'metathingy'));
$this->assertEquals(0, count($this->observer->get_attempts_added()));
$this->assertEquals(0, count($this->observer->get_attempts_modified()));
$this->assertEquals(0, count($this->observer->get_steps_added()));
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(array($this->slot => array('metathingy' => $this->quba->get_question_attempt($this->slot))),
$this->observer->get_metadata_added());
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_set_metadata_previously_set_but_dont_actually_change() {
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'a value');
$this->observer = new testable_question_engine_unit_of_work($this->quba);
$this->quba->set_observer($this->observer);
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'a value');
$this->assertEquals('a value', $this->quba->get_question_attempt_metadata($this->slot, 'metathingy'));
$this->assertEquals(0, count($this->observer->get_attempts_added()));
$this->assertEquals(0, count($this->observer->get_attempts_modified()));
$this->assertEquals(0, count($this->observer->get_steps_added()));
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_set_metadata_previously_set() {
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'a value');
$this->observer = new testable_question_engine_unit_of_work($this->quba);
$this->quba->set_observer($this->observer);
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'different value');
$this->assertEquals('different value', $this->quba->get_question_attempt_metadata($this->slot, 'metathingy'));
$this->assertEquals(0, count($this->observer->get_attempts_added()));
$this->assertEquals(0, count($this->observer->get_attempts_modified()));
$this->assertEquals(0, count($this->observer->get_steps_added()));
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(array($this->slot => array('metathingy' => $this->quba->get_question_attempt($this->slot))),
$this->observer->get_metadata_modified());
}
public function test_set_metadata_in_new_question() {
$newslot = $this->quba->add_question(test_question_maker::make_question('truefalse'));
$this->quba->start_question($newslot);
$this->quba->set_question_attempt_metadata($newslot, 'metathingy', 'a value');
$this->assertEquals('a value', $this->quba->get_question_attempt_metadata($newslot, 'metathingy'));
$this->assertEquals(array($newslot => $this->quba->get_question_attempt($newslot)),
$this->observer->get_attempts_added());
$this->assertEquals(0, count($this->observer->get_attempts_modified()));
$this->assertEquals(0, count($this->observer->get_steps_added()));
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(0, count($this->observer->get_metadata_added()));
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_set_metadata_then_move() {
$this->quba->set_question_attempt_metadata($this->slot, 'metathingy', 'a value');
$q = test_question_maker::make_question('truefalse');
$newslot = $this->quba->add_question_in_place_of_other($this->slot, $q);
$this->quba->start_question($this->slot);
$this->assertEquals('a value', $this->quba->get_question_attempt_metadata($newslot, 'metathingy'));
$this->assertEquals(array($this->slot => $this->quba->get_question_attempt($this->slot)),
$this->observer->get_attempts_added());
$this->assertEquals(array($newslot => $this->quba->get_question_attempt($newslot)),
$this->observer->get_attempts_modified());
$this->assertEquals(0, count($this->observer->get_steps_added()));
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(array($newslot => array('metathingy' => $this->quba->get_question_attempt($newslot))),
$this->observer->get_metadata_added());
$this->assertEquals(0, count($this->observer->get_metadata_modified()));
}
public function test_move_then_set_metadata() {
$q = test_question_maker::make_question('truefalse');
$newslot = $this->quba->add_question_in_place_of_other($this->slot, $q);
$this->quba->start_question($this->slot);
$this->quba->set_question_attempt_metadata($newslot, 'metathingy', 'a value');
$this->assertEquals('a value', $this->quba->get_question_attempt_metadata($newslot, 'metathingy'));
$this->assertEquals(array($this->slot => $this->quba->get_question_attempt($this->slot)),
$this->observer->get_attempts_added());
$this->assertEquals(array($newslot => $this->quba->get_question_attempt($newslot)),
$this->observer->get_attempts_modified());
$this->assertEquals(0, count($this->observer->get_steps_added()));
$this->assertEquals(0, count($this->observer->get_steps_modified()));
$this->assertEquals(0, count($this->observer->get_steps_deleted()));
$this->assertEquals(array($newslot => array('metathingy' => $this->quba->get_question_attempt($newslot))),
$this->observer->get_metadata_added());
}
}

View File

@ -1,18 +1,47 @@
This files describes API changes for the core question system.
This files describes API changes for the core question engine.
=== 2.9 ===
1) Some new methods on the question_usage class (and corresponding methods on
question_attempt, question_attempt_step, question_usage_observer, ... requried
to implement them, but almost certainly you should only be calling the
question_usage methods from your code.
* question_usage::add_question_in_place_of_other($slot, $question, $maxmark = null)
This creates a new questoin_attempt in place of an existing one, moving the
existing question_attempt to the end of the usage, in a new slot number.
The new slot number is returned. The goal is to replace the old attempt, but
not lose the old data.
* question_usage::set_question_max_mark($slot, $maxmark)
Sets the max mark for one question in this usage. Previously, you could
only change this using the bulk operation question_usage::set_max_mark_in_attempts;
* question_usage::set_question_attempt_metadata($slot, $name, $value);
question_usage::get_question_attempt_metadata($slot, $name);
You can now record metadata, that is, values stored by name, against
question_attempts. The question engine ignores this data (other than storing
and loading it) but you may find it useful in your code.
To see examples of where these are used, look at the chagnes from MDL-40992.
=== 2.6 ===
1) The method question_behaviour::is_manual_grade_in_range and move and become
question_engine::is_manual_grade_in_range.
question_engine::is_manual_grade_in_range.
2) The arguments to core_question_renderer::mark_summary changed from
($qa, $options) to ($qa, $behaviouroutput, $options). If you have overridden
that method you will need to update your code.
($qa, $options) to ($qa, $behaviouroutput, $options). If you have overridden
that method you will need to update your code.
3) Heading level for number(), add_part_heading() and respond_history()
has been lowered by one level. These changes are part of improving the page
accessibility and making heading to have proper nesting. (MDL-41615)
has been lowered by one level. These changes are part of improving the page
accessibility and making heading to have proper nesting. (MDL-41615)
=== Earlier changes ===

View File

@ -22,7 +22,7 @@ This files describes API changes for code that uses the question API.
=== 2.8 ===
1) This is jsut a warning that some methods of the question_engine_data_mapper
1) This is just a warning that some methods of the question_engine_data_mapper
class have changed. All these methods are ones that you should not have been
calling directly from your code, so this should not cause any problems.
The changed methods are: