Skip to content

Commit

Permalink
MDL-40988 quiz: ability to break quizzes into sections
Browse files Browse the repository at this point in the history
The sections are created on the edit quiz page, and then appear in the
navigation panel when the quiz is being attempted to help students find
their way around.

The 'Shuffle questions' setting has been moved from being per-quiz
to being a per-section.

This commit is actually the joint work of Mahmoud Kassaei and Tim Hunt
from The Open University. We could only use one persons name for the
commit and this time Mahmoud gets the credit/blame.
  • Loading branch information
mkassaei authored and timhunt committed Apr 5, 2015
1 parent 1d3fd63 commit 5d94970
Show file tree
Hide file tree
Showing 60 changed files with 3,342 additions and 1,246 deletions.
120 changes: 114 additions & 6 deletions mod/quiz/attemptlib.php
Expand Up @@ -60,15 +60,22 @@ public function __construct($quizobj, $errorcode, $a = null, $link = '', $debugi
* @since Moodle 2.0
*/
class quiz {
// Fields initialised in the constructor.
/** @var stdClass the course settings from the database. */
protected $course;
/** @var stdClass the course_module settings from the database. */
protected $cm;
/** @var stdClass the quiz settings from the database. */
protected $quiz;
/** @var context the quiz context. */
protected $context;

// Fields set later if that data is needed.
/** @var array of questions augmented with slot information. */
protected $questions = null;
/** @var array of quiz_section rows. */
protected $sections = null;
/** @var quiz_access_manager the access manager for this quiz. */
protected $accessmanager = null;
/** @var bool whether the current user has capability mod/quiz:preview. */
protected $ispreviewuser = null;

// Constructor =============================================================
Expand Down Expand Up @@ -262,6 +269,21 @@ public function get_questions($questionids = null) {
}

/**
* Get all the sections in this quiz.
* @return array 0, 1, 2, ... => quiz_sections row from the database.
*/
public function get_sections() {
global $DB;
if ($this->sections === null) {
$this->sections = array_values($DB->get_records('quiz_sections',
array('quizid' => $this->get_quizid()), 'firstslot'));
}
return $this->sections;
}

/**
* Return quiz_access_manager and instance of the quiz_access_manager class
* for this quiz at this time.
* @param int $timenow the current time as a unix timestamp.
* @return quiz_access_manager and instance of the quiz_access_manager class
* for this quiz at this time.
Expand Down Expand Up @@ -455,9 +477,18 @@ class quiz_attempt {
/** @var question_usage_by_activity the question usage for this quiz attempt. */
protected $quba;

/** @var array of quiz_slots rows. */
/**
* @var array of slot information. These objects contain ->slot (int),
* ->requireprevious (bool), ->questionids (int) the original question for random questions,
* ->firstinsection (bool), ->section (stdClass from $this->sections).
* This does not contain page - get that from {@link get_question_page()} -
* or maxmark - get that from $this->quba.
*/
protected $slots;

/** @var array of quiz_sections rows, with a ->lastslot field added. */
protected $sections;

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

Expand Down Expand Up @@ -494,8 +525,11 @@ 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');
'slot, requireprevious, questionid');
$this->sections = array_values($DB->get_records('quiz_sections',
array('quizid' => $this->get_quizid()), 'firstslot'));

$this->link_sections_and_slots();
$this->determine_layout();
$this->number_questions();
}
Expand Down Expand Up @@ -546,6 +580,22 @@ public static function state_name($state) {
return quiz_attempt_state_name($state);
}

/**
* Let each slot know which section it is part of.
*/
protected function link_sections_and_slots() {
foreach ($this->sections as $i => $section) {
if (isset($this->sections[$i + 1])) {
$section->lastslot = $this->sections[$i + 1]->firstslot - 1;
} else {
$section->lastslot = count($this->slots);
}
for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) {
$this->slots[$slot]->section = $section;
}
}
}

/**
* Parse attempt->layout to populate the other arrays the represent the layout.
*/
Expand All @@ -561,13 +611,27 @@ protected function determine_layout() {
}

// File the ids into the arrays.
// Tracking which is the first slot in each section in this attempt is
// trickier than you might guess, since the slots in this section
// may be shuffled, so $section->firstslot (the lowest numbered slot in
// the section) may not be the first one.
$unseensections = $this->sections;
$this->pagelayout = array();
foreach ($pagelayouts as $page => $pagelayout) {
$pagelayout = trim($pagelayout, ',');
if ($pagelayout == '') {
continue;
}
$this->pagelayout[$page] = explode(',', $pagelayout);
foreach ($this->pagelayout[$page] as $slot) {
$sectionkey = array_search($this->slots[$slot]->section, $unseensections);
if ($sectionkey !== false) {
$this->slots[$slot]->firstinsection = true;
unset($unseensections[$sectionkey]);
} else {
$this->slots[$slot]->firstinsection = false;
}
}
}
}

Expand Down Expand Up @@ -1038,7 +1102,8 @@ public function is_question_flagged($slot) {
*/
public function is_blocked_by_previous_question($slot) {
return $slot > 1 && isset($this->slots[$slot]) && $this->slots[$slot]->requireprevious &&
!$this->get_quiz()->shufflequestions &&
!$this->slots[$slot]->section->shufflequestions &&
!$this->slots[$slot - 1]->section->shufflequestions &&
$this->get_navigation_method() != QUIZ_NAVMETHOD_SEQ &&
!$this->get_question_state($slot - 1)->is_finished() &&
$this->quba->can_question_finish_during_attempt($slot - 1);
Expand Down Expand Up @@ -1081,6 +1146,20 @@ public function get_question_number($slot) {
}

/**
* If the section heading, if any, that should come just before this slot.
* @param int $slot identifies a particular question in this attempt.
* @return string the required heading, or null if there is not one here.
*/
public function get_heading_before_slot($slot) {
if ($this->slots[$slot]->firstinsection) {
return $this->slots[$slot]->section->heading;
} else {
return null;
}
}

/**
* Return the page of the quiz where this question appears.
* @param int $slot the number used to identify this question within this attempt.
* @return int the page of the quiz this question appears on.
*/
Expand Down Expand Up @@ -1809,7 +1888,7 @@ public function update_timecheckstate($time) {
global $DB;
if ($this->attempt->timecheckstate !== $time) {
$this->attempt->timecheckstate = $time;
$DB->set_field('quiz_attempts', 'timecheckstate', $time, array('id'=>$this->attempt->id));
$DB->set_field('quiz_attempts', 'timecheckstate', $time, array('id' => $this->attempt->id));
}
}

Expand Down Expand Up @@ -1965,6 +2044,27 @@ protected function page_and_question_url($script, $slot, $page, $showall, $thisp
}


/**
* Represents a heading in the navigation panel.
*
* @copyright 2015 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 2.9
*/
class quiz_nav_section_heading implements renderable {
/** @var string the heading text. */
public $heading;

/**
* Constructor.
* @param string $heading the heading text
*/
public function __construct($heading) {
$this->heading = $heading;
}
}


/**
* Represents a single link in the navigation panel.
*
Expand Down Expand Up @@ -2018,9 +2118,17 @@ public function __construct(quiz_attempt $attemptobj,
$this->showall = $showall;
}

/**
* Get the buttons and section headings to go in the quiz navigation block.
* @return renderable[] the buttons, possibly interleaved with section headings.
*/
public function get_question_buttons() {
$buttons = array();
foreach ($this->attemptobj->get_slots() as $slot) {
if ($heading = $this->attemptobj->get_heading_before_slot($slot)) {
$buttons[] = new quiz_nav_section_heading(format_string($heading));
}

$qa = $this->attemptobj->get_question_attempt($slot);
$showcorrectness = $this->options->correctness && $qa->has_marks();

Expand Down
13 changes: 12 additions & 1 deletion mod/quiz/backup/moodle2/backup_quiz_stepslib.php
Expand Up @@ -46,7 +46,7 @@ protected function define_structure() {
'reviewattempt', 'reviewcorrectness', 'reviewmarks',
'reviewspecificfeedback', 'reviewgeneralfeedback',
'reviewrightanswer', 'reviewoverallfeedback',
'questionsperpage', 'navmethod', 'shufflequestions', 'shuffleanswers',
'questionsperpage', 'navmethod', 'shuffleanswers',
'sumgrades', 'grade', 'timecreated',
'timemodified', 'password', 'subnet', 'browsersecurity',
'delay1', 'delay2', 'showuserpicture', 'showblocks', 'completionattemptsexhausted', 'completionpass'));
Expand All @@ -59,6 +59,11 @@ protected function define_structure() {
$qinstance = new backup_nested_element('question_instance', array('id'), array(
'slot', 'page', 'requireprevious', 'questionid', 'maxmark'));

$sections = new backup_nested_element('sections');

$section = new backup_nested_element('section', array('id'), array(
'firstslot', 'heading', 'shufflequestions'));

$feedbacks = new backup_nested_element('feedbacks');

$feedback = new backup_nested_element('feedback', array('id'), array(
Expand Down Expand Up @@ -92,6 +97,9 @@ protected function define_structure() {
$quiz->add_child($qinstances);
$qinstances->add_child($qinstance);

$quiz->add_child($sections);
$sections->add_child($section);

$quiz->add_child($feedbacks);
$feedbacks->add_child($feedback);

Expand All @@ -110,6 +118,9 @@ protected function define_structure() {
$qinstance->set_source_table('quiz_slots',
array('quizid' => backup::VAR_PARENTID));

$section->set_source_table('quiz_sections',
array('quizid' => backup::VAR_PARENTID));

$feedback->set_source_table('quiz_feedback',
array('quizid' => backup::VAR_PARENTID));

Expand Down
32 changes: 32 additions & 0 deletions mod/quiz/backup/moodle2/restore_quiz_stepslib.php
Expand Up @@ -33,6 +33,19 @@
*/
class restore_quiz_activity_structure_step extends restore_questions_activity_structure_step {

/**
* @var bool tracks whether the quiz contains at least one section. Before
* Moodle 2.9 quiz sections did not exist, so if the file being restored
* did not contain any, we need to create one in {@link after_execute()}.
*/
protected $sectioncreated = false;

/**
* @var bool when restoring old quizzes (2.8 or before) this records the
* shufflequestionsoption quiz option which has moved to the quiz_sections table.
*/
protected $legacyshufflequestionsoption = false;

protected function define_structure() {

$paths = array();
Expand All @@ -46,6 +59,7 @@ protected function define_structure() {

$paths[] = new restore_path_element('quiz_question_instance',
'/activity/quiz/question_instances/question_instance');
$paths[] = new restore_path_element('quiz_section', '/activity/quiz/sections/section');
$paths[] = new restore_path_element('quiz_feedback', '/activity/quiz/feedbacks/feedback');
$paths[] = new restore_path_element('quiz_override', '/activity/quiz/overrides/override');

Expand Down Expand Up @@ -276,6 +290,15 @@ protected function process_quiz_question_instance($data) {
$DB->insert_record('quiz_slots', $data);
}

protected function process_quiz_section($data) {
global $DB;

$data = (object) $data;
$data->quizid = $this->get_new_parentid('quiz');
$newitemid = $DB->insert_record('quiz_sections', $data);
$this->sectioncreated = true;
}

protected function process_quiz_feedback($data) {
global $DB;

Expand Down Expand Up @@ -387,10 +410,19 @@ protected function inform_new_usage_id($newusageid) {
}

protected function after_execute() {
global $DB;

parent::after_execute();
// Add quiz related files, no need to match by itemname (just internally handled context).
$this->add_related_files('mod_quiz', 'intro', null);
// Add feedback related files, matching by itemname = 'quiz_feedback'.
$this->add_related_files('mod_quiz', 'feedback', 'quiz_feedback');

if (!$this->sectioncreated) {
$DB->insert_record('quiz_sections', array(
'quizid' => $this->get_new_parentid('quiz'),
'firstslot' => 1, 'heading' => '',
'shufflequestions' => $this->legacyshufflequestionsoption));
}
}
}

0 comments on commit 5d94970

Please sign in to comment.