Skip to content

Commit

Permalink
MDL-40990 quiz: option to require prev Q finished before next shown
Browse files Browse the repository at this point in the history
Further improvements to this code, including resolving edge cases:

* The new feature can only be used when it is possible for the
previous question in the quiz to be complete.

* Also, this new feature cannot be used in combination with shuffle
questions, because that make no sense; nor in combination with
sequential navigation, because to make that work properly would be a lot
of effort. If someone needs that to work later, it should be possible
for them to implement it.

* There were changes in the edit renderer API, to try to make things
more  consistent, and to make it less likely we will need to change
things again in the future. See mod/quiz/upgrade.txt.

* As part of this change, the styling of the Edit quiz page was tweaked
to make slighly more efficient use of the horizontal space, and to be
more symmetrical.
  • Loading branch information
timhunt committed Mar 17, 2015
1 parent f7785e4 commit 441d284
Show file tree
Hide file tree
Showing 29 changed files with 1,248 additions and 854 deletions.
110 changes: 79 additions & 31 deletions mod/quiz/attemptlib.php
Original file line number Diff line number Diff line change
Expand Up @@ -446,13 +446,18 @@ class quiz_attempt {
/** @var int maximum number of slots in the quiz for the review page to default to show all. */
const MAX_SLOTS_FOR_DEFAULT_REVIEW_SHOW_ALL = 50;

// Basic data.
/** @var quiz object containing the quiz settings. */
protected $quizobj;

/** @var stdClass the quiz_attempts row. */
protected $attempt;

/** @var question_usage_by_activity the question usage for this quiz attempt. */
protected $quba;

/** @var array of quiz_slots rows. */
protected $slots;

/** @var array page no => array of slot numbers on the page in order. */
protected $pagelayout;

Expand All @@ -477,6 +482,8 @@ class quiz_attempt {
* of the state of each question. Else just set up the basic details of the attempt.
*/
public function __construct($attempt, $quiz, $cm, $course, $loadquestions = true) {
global $DB;

$this->attempt = $attempt;
$this->quizobj = new quiz($quiz, $cm, $course);

Expand All @@ -485,6 +492,10 @@ public function __construct($attempt, $quiz, $cm, $course, $loadquestions = true
}

$this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid);
$this->slots = $DB->get_records('quiz_slots',
array('quizid' => $this->get_quizid()), 'slot',
'slot, page, requireprevious, questionid, maxmark');

$this->determine_layout();
$this->number_questions();
}
Expand Down Expand Up @@ -987,6 +998,21 @@ public function is_question_flagged($slot) {
}

/**
* Checks whether the question in this slot requires the previous question to have been completed.
*
* @param int $slot the number used to identify this question within this attempt.
* @return bool whether the previous question must have been completed before this one can be seen.
*/
public function is_blocked_by_previous_question($slot) {
return $slot > 1 && $this->slots[$slot]->requireprevious &&
!$this->get_quiz()->shufflequestions &&
$this->get_navigation_method() != QUIZ_NAVMETHOD_SEQ &&
!$this->quba->get_question_state($slot - 1)->is_finished() &&
$this->quba->can_question_finish_during_attempt($slot - 1);
}

/**
* Get the displayed question number for a slot.
* @param int $slot the number used to identify this question within this attempt.
* @return string the displayed question number for the question in this slot.
* For example '1', '2', '3' or 'i'.
Expand Down Expand Up @@ -1168,36 +1194,6 @@ public function processattempt_url() {
return new moodle_url('/mod/quiz/processattempt.php');
}

/**
* Return slot object for the given slotnumber in a given quizid
*
* @param int $quizid
* @param int $slotnumber
*/
public function get_slot_object($quizid, $slotnumber) {
global $DB;
return $DB->get_record('quiz_slots', array('slot' => $slotnumber, 'quizid' => $quizid));
}

/**
* Checks whether it requires previous question. If the previous question is not completed
* return a message in descripyiom question type format, otherwise returns null
*
* @param int $slot
*/
public function require_previous_question($slot) {
$quiz = $this->get_quiz();
$currentslot = $this->get_slot_object($quiz->id, $slot);
$previousslot = $this->get_slot_object($quiz->id, $currentslot->slot - 1);

if ($currentslot->requireprevious && $previousslot) {
if ($this->get_question_status($previousslot->slot, false) == 'Not yet answered') {
return $this->quba->replace_question_with_a_description_qtye($currentslot);
}
}
return null;
}

/**
* @param int $slot indicates which question to link to.
* @param int $page if specified, the URL of this particular page of the attempt, otherwise
Expand Down Expand Up @@ -1285,11 +1281,63 @@ public function restart_preview_button() {
* @return string HTML for the question in its current state.
*/
public function render_question($slot, $reviewing, $thispageurl = null) {
if ($this->is_blocked_by_previous_question($slot)) {
$placeholderqa = $this->make_blocked_question_placeholder($slot);

$displayoptions = $this->get_display_options($reviewing);
$displayoptions->manualcomment = question_display_options::HIDDEN;
$displayoptions->history = question_display_options::HIDDEN;
$displayoptions->readonly = true;

return html_writer::div($placeholderqa->render($displayoptions,
$this->get_question_number($slot)),
'mod_quiz-blocked_question_warning');
}

return $this->quba->render_question($slot,
$this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl),
$this->get_question_number($slot));
}

/**
* Create a fake question to be displayed in place of a question that is blocked
* until the previous question has been answered.
*
* @param unknown $slot int slot number of the question to replace.
* @return question_definition the placeholde question.
*/
protected function make_blocked_question_placeholder($slot) {
$replacedquestion = $this->get_question_attempt($slot)->get_question();

question_bank::load_question_definition_classes('description');
$question = new qtype_description_question();
$question->id = $replacedquestion->id;
$question->category = null;
$question->parent = 0;
$question->qtype = question_bank::get_qtype('description');
$question->name = '';
$question->questiontext = get_string('questiondependsonprevious', 'quiz');
$question->questiontextformat = FORMAT_HTML;
$question->generalfeedback = '';
$question->defaultmark = $this->quba->get_question_max_mark($slot);
$question->length = $replacedquestion->length;
$question->penalty = 0;
$question->stamp = '';
$question->version = 0;
$question->hidden = 0;
$question->timecreated = null;
$question->timemodified = null;
$question->createdby = null;
$question->modifiedby = null;

$placeholderqa = new question_attempt($question, $this->quba->get_id(),
null, $this->quba->get_question_max_mark($slot));
$placeholderqa->set_slot($slot);
$placeholderqa->start($this->get_quiz()->preferredbehaviour, 1);
$placeholderqa->set_flagged($this->is_question_flagged($slot));
return $placeholderqa;
}

/**
* Like {@link render_question()} but displays the question at the past step
* indicated by $seq, rather than showing the latest step.
Expand Down
2 changes: 1 addition & 1 deletion mod/quiz/backup/moodle2/backup_quiz_stepslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ protected function define_structure() {
$qinstances = new backup_nested_element('question_instances');

$qinstance = new backup_nested_element('question_instance', array('id'), array(
'slot', 'page', 'questionid', 'maxmark'));
'slot', 'page', 'requireprevious', 'questionid', 'maxmark'));

$feedbacks = new backup_nested_element('feedbacks');

Expand Down
Loading

0 comments on commit 441d284

Please sign in to comment.