Skip to content

Commit

Permalink
MDL-80880 quiz: change display of previous attempts summary
Browse files Browse the repository at this point in the history
Rather than a table (neither reponsive nor very accesible) we now
represent a student's previous attempts with cards. The content
of the cards is now the same table as at the top of the review
attempt page.
  • Loading branch information
timhunt committed Mar 5, 2024
1 parent 8feeacc commit a57d88b
Show file tree
Hide file tree
Showing 13 changed files with 190 additions and 82 deletions.
20 changes: 14 additions & 6 deletions mod/quiz/classes/output/attempt_summary_information.php
Expand Up @@ -17,6 +17,7 @@
namespace mod_quiz\output;

use action_link;
use core\output\named_templatable;
use html_writer;
use mod_quiz\quiz_attempt;
use moodle_url;
Expand All @@ -25,7 +26,6 @@
use renderable;
use renderer_base;
use stdClass;
use templatable;
use user_picture;

/**
Expand All @@ -34,12 +34,13 @@
* This is used in places like
* - at the top of the review attempt page (review.php)
* - at the top of the review single question page (reviewquestion.php)
* - on the quiz entry page (view.php).
*
* @package mod_quiz
* @copyright 2024 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_summary_information implements renderable, templatable {
class attempt_summary_information implements renderable, named_templatable {

/** @var array[] The rows of summary data. {@see add_item()} should make the structure clear. */
protected array $summarydata = [];
Expand Down Expand Up @@ -90,7 +91,8 @@ public static function create_from_legacy_array(array $items): static {
*
* @param quiz_attempt $attemptobj the attempt to summarise.
* @param display_options $options options for what can be seen.
* @param int $page if specified, the URL of this particular page of the attempt, otherwise
* @param int|null $pageforlinkingtootherattempts if null, no links to other attempsts will be created.
* If specified, the URL of this particular page of the attempt, otherwise
* the URL will go to the first page. If -1, deduce $page from $slot.
* @param bool|null $showall if true, the URL will be to review the entire attempt on one page,
* and $page will be ignored. If null, a sensible default will be chosen.
Expand All @@ -99,7 +101,7 @@ public static function create_from_legacy_array(array $items): static {
public static function create_for_attempt(
quiz_attempt $attemptobj,
display_options $options,
int $page = -1,
?int $pageforlinkingtootherattempts = null,
?bool $showall = null,
): static {
global $DB, $USER;
Expand All @@ -119,9 +121,9 @@ public static function create_for_attempt(
);
}

if ($attemptobj->has_capability('mod/quiz:viewreports')) {
if ($pageforlinkingtootherattempts !== null && $attemptobj->has_capability('mod/quiz:viewreports')) {
$attemptlist = $attemptobj->links_to_other_attempts(
$attemptobj->review_url(null, $page, $showall));
$attemptobj->review_url(null, $pageforlinkingtootherattempts, $showall));
if ($attemptlist) {
$summary->add_item('attemptlist', get_string('attempts', 'quiz'), $attemptlist);
}
Expand Down Expand Up @@ -239,4 +241,10 @@ public function export_for_template(renderer_base $output): array {

return $templatecontext;
}

public function get_template_name(\renderer_base $renderer): string {
// Only reason we are forced to implement this is that we want the quiz renderer
// passed to export_for_template, not a core_renderer.
return 'mod_quiz/attempt_summary_information';
}
}
85 changes: 85 additions & 0 deletions mod/quiz/classes/output/list_of_attempts.php
@@ -0,0 +1,85 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace mod_quiz\output;

use core\output\named_templatable;
use mod_quiz\quiz_attempt;
use renderable;
use renderer_base;

/**
* Display summary information about a list of attempts.
*
* This is used on the front page of the quiz (view.php).
*
* @package mod_quiz
* @copyright 2024 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class list_of_attempts implements renderable, named_templatable {

/** @var int time to consider as now. */
protected int $timenow;

/** @var quiz_attempt[] The list of attempts to summarise. */
protected array $attempts = [];

/**
* Constructor.
*
* @param int $timenow time that is now.
*/
public function __construct(int $timenow) {
$this->timenow = $timenow;
}

/**
* Add an event to the list.
*
* @param quiz_attempt $attemptobj
*/
public function add_attempt(quiz_attempt $attemptobj): void {
$this->attempts[] = $attemptobj;
}

public function export_for_template(renderer_base $output): array {

$templatecontext = [
'hasattempts' => !empty($this->attempts),
'attempts' => [],
];

foreach ($this->attempts as $attemptobj) {
$displayoptions = $attemptobj->get_display_options(true);
$templatecontext['attempts'][] = (object) [
'name' => get_string('attempt', 'mod_quiz', $attemptobj->get_attempt_number()),
'summarydata' => attempt_summary_information::create_for_attempt(
$attemptobj, $displayoptions)->export_for_template($output),
'reviewlink' => $attemptobj->get_access_manager($this->timenow)->make_review_link(
$attemptobj->get_attempt(), $displayoptions, $output),
];
}

return $templatecontext;
}

public function get_template_name(\renderer_base $renderer): string {
// Only reason we are forced to implement this is that we want the quiz renderer
// passed to export_for_template, not a core_renderer.
return 'mod_quiz/list_of_attempts';
}
}
4 changes: 3 additions & 1 deletion mod/quiz/classes/output/renderer.php
Expand Up @@ -889,8 +889,8 @@ public function view_page($course, $quiz, $cm, $context, $viewobj) {

$output .= $this->view_page_tertiary_nav($viewobj);
$output .= $this->view_information($quiz, $cm, $context, $viewobj->infomessages);
$output .= $this->view_table($quiz, $context, $viewobj);
$output .= $this->view_result_info($quiz, $context, $cm, $viewobj);
$output .= $this->render($viewobj->attemptslist);
$output .= $this->box($this->view_page_buttons($viewobj), 'quizattempt');
return $output;
}
Expand Down Expand Up @@ -1115,8 +1115,10 @@ public function view_table_heading() {
* @param stdClass $quiz the quiz settings.
* @param context_module $context the quiz context.
* @param view_page $viewobj
* @deprecated Since 4.4 please use the {@see list_of_attempts} renderable instead.
*/
public function view_table($quiz, $context, $viewobj) {
debugging('view_table has been deprecated since 4.4 please use the list_of_attempts renderable instead.');
if (!$viewobj->attempts) {
return '';
}
Expand Down
2 changes: 2 additions & 0 deletions mod/quiz/classes/output/view_page.php
Expand Up @@ -39,6 +39,8 @@ class view_page {
public $attempts;
/** @var quiz_attempt[] $attemptobjs objects corresponding to $attempts. */
public $attemptobjs;
/** @var list_of_attempts list of past attempts for rendering. */
public $attemptslist;
/** @var access_manager $accessmanager contains various access rules. */
public $accessmanager;
/** @var bool $canreviewmine whether the current user has the capability to
Expand Down
5 changes: 5 additions & 0 deletions mod/quiz/classes/quiz_attempt.php
Expand Up @@ -593,10 +593,15 @@ public function requires_manual_grading(): bool {
* The values are arrays with two items, title and content. Each of these
* will be either a string, or a renderable.
*
* If this method is called before load_questions() is called, then an empty array is returned.
*
* @param question_display_options $options the display options for this quiz attempt at this time.
* @return array as described above.
*/
public function get_additional_summary_data(question_display_options $options) {
if (!isset($this->quba)) {
return [];
}
return $this->quba->get_summary_information($options);
}

Expand Down
2 changes: 1 addition & 1 deletion mod/quiz/lang/en/quiz.php
Expand Up @@ -1016,7 +1016,7 @@
$string['subplugintype_quizaccess_plural'] = 'Access rules';
$string['substitutedby'] = 'will be substituted by';
$string['summaryofattempt'] = 'Summary of attempt';
$string['summaryofattempts'] = 'Summary of your previous attempts';
$string['summaryofattempts'] = 'Your attempts';
$string['temporaryblocked'] = 'You are temporarily not allowed to re-attempt the quiz.<br /> You will be able to take another attempt on:';
$string['theattempt'] = 'The attempt';
$string['theattempt_help'] = 'Whether the student can review the attempt at all.';
Expand Down
18 changes: 0 additions & 18 deletions mod/quiz/styles.css
Expand Up @@ -251,24 +251,6 @@
text-align: center;
}

#page-mod-quiz-view #page .quizattemptsummary td p {
margin-top: 0;
}

#page-mod-quiz-view table.quizattemptsummary tr.bestrow td {
border-color: #bce8f1;
background-color: #d9edf7;
}

table.quizattemptsummary .noreviewmessage {
color: gray;
}

#page-mod-quiz-view .generaltable.quizattemptsummary {
margin-left: auto;
margin-right: auto;
}

#page-mod-quiz-view .generalbox#feedback .overriddennotice {
text-align: center;
font-size: 0.7em;
Expand Down
68 changes: 68 additions & 0 deletions mod/quiz/templates/list_of_attempts.mustache
@@ -0,0 +1,68 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template mod_quiz/list_of_attempts
This template renders summary information about a list of quiz attempts.
The structure for each attempt should be what is required by mod_quiz/attempt_summary_information.
Example context (json):
{
"hasattempts": true,
"attempts": [
{
"name": "Attempt 1",
"reviewlink": "<a href='https://qa.moodledemo.net/mod/quiz/review.php?attempt=13&cmid=30'>Review</a>",
"summarydata": {
"hasitems": true,
"items": [
{
"title": "<img src='https://qa.moodledemo.net/pluginfile.php/27/user/icon/boost/f2?rev=5522' class='userpicture' width='35' height='35' alt='It is me!'>",
"content": "<a href='https://qa.moodledemo.net/user/view.php?id=4&amp;course=2'>Sam Student</a>"
},
{"title": "State", "content": "Finished"},
{"title": "Started on", "content": "Thursday, 23 November 2023, 9:29 AM"},
{"title": "Completed on", "content": "Thursday, 23 November 2023, 9:32 AM"},
{"title": "Grade", "content": "Not yet graded"}
]
}
}
]
}
}}
{{#hasattempts}}
<h3>{{# str}}summaryofattempts, quiz{{/str}}</h3>

<ul class="list-unstyled row row-cols-1 row-cols-md-2 no-gutters">

{{#attempts}}
<li class="col pl-0 pr-2 mb-2">
<div class="card h-100">
<div class="card-header py-2 border-bottom-0">
<h4 class="card-title my-0">{{name}}</h4>
</div>
{{#summarydata}}
{{> mod_quiz/attempt_summary_information}}
{{/summarydata}}
<div class="card-body py-2">
<div>{{{reviewlink}}}</div>
</div>
</div>
</li>
{{/attempts}}

</ul>

{{/hasattempts}}
52 changes: 0 additions & 52 deletions mod/quiz/tests/attempt_test.php
Expand Up @@ -507,56 +507,4 @@ public function test_quiz_start_attempt_built_on_last_with_draft(): void {
$this->expectExceptionObject(new \moodle_exception('questiondraftonly', 'mod_quiz', '', $question->name));
quiz_start_attempt_built_on_last($quba, $newattempt, $attempt);
}

/**
* Starting a new attempt and check the summary previous attempts table.
*
* @covers ::view_table()
*/
public function test_view_table(): void {
global $PAGE;
$this->resetAfterTest();

$timenow = time();
// Create attempt object.
$attempt = $this->create_quiz_and_attempt_with_layout('1,1,0');
// Finish attempt.
$attempt->process_finish($timenow, false);

$quiz = $attempt->get_quiz();
$context = $attempt->get_context();

// Prepare view object.
$viewobj = new view_page();
$viewobj->attemptcolumn = true;
$viewobj->markcolumn = true;
$viewobj->gradecolumn = true;
$viewobj->canreviewmine = true;
$viewobj->mygrade = 0.00;
$viewobj->feedbackcolumn = false;
$viewobj->attempts = $attempt;
$viewobj->attemptobjs[] = new quiz_attempt($attempt->get_attempt(),
$quiz, $attempt->get_cm(), $attempt->get_course(), false);
$viewobj->accessmanager = new access_manager($attempt->get_quizobj(), $timenow,
has_capability('mod/quiz:ignoretimelimits', $context, null, false));

// Render summary previous attempts table.
$renderer = $PAGE->get_renderer('mod_quiz');
$table = $renderer->view_table($quiz, $context, $viewobj);
$captionpattern = '/<caption\b[^>]*>' . get_string('summaryofattempts', 'quiz') . '<\/caption>/';

// Check caption existed.
$this->assertMatchesRegularExpression($captionpattern, $table);
// Check column attempt.
$this->assertMatchesRegularExpression('/<td\b[^>]*>' . $attempt->get_attempt_number() . '<\/td>/', $table);
// Check column state.
$this->assertMatchesRegularExpression('/<td\b[^>]*>' . ucfirst($attempt->get_state()) . '.+?<\/td>/', $table);
// Check column marks.
$this->assertMatchesRegularExpression('/<td\b[^>]* c2.+?' .
quiz_format_grade($quiz, $attempt->get_sum_marks()) .'<\/td>/', $table);
// Check column grades.
$this->assertMatchesRegularExpression('/<td\b[^>]* c2.+?0\.00<\/td>/', $table);
// Check column review.
$this->assertMatchesRegularExpression('/<td\b[^>]*>.+?Review<\/a><\/td>/', $table);
}
}
4 changes: 2 additions & 2 deletions mod/quiz/tests/behat/preview.feature
Expand Up @@ -40,7 +40,7 @@ Feature: Preview a quiz as a teacher
Then I should see "25.00 out of 100.00"
And I should see "v1 (latest)" in the "Question 1" "question"
And I follow "Finish review"
And "Review" "link" in the "Preview" "table_row" should be visible
And "Review" "link" in the "Attempt 1" "list_item" should be visible

@javascript
Scenario: Review the quiz attempt with custom decimal separator
Expand All @@ -53,7 +53,7 @@ Feature: Preview a quiz as a teacher
And I should see "25#00 out of 100#00"
And I should see "Mark 1#00 out of 1#00"
And I follow "Finish review"
And "Review" "link" in the "Preview" "table_row" should be visible
And "Review" "link" in the "Attempt 1" "list_item" should be visible

Scenario: Preview the quiz
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "teacher"
Expand Down
6 changes: 4 additions & 2 deletions mod/quiz/tests/behat/reattempt_quiz.feature
Expand Up @@ -51,5 +51,7 @@ Feature: Several attempts in a quiz
Scenario: The redo question buttons are visible after 2 attempts are preset for student1.
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student1"
Then "Re-attempt quiz" "button" should exist
And "1" row "Marks / 2.00" column of "quizattemptsummary" table should contain "1.00"
And "2" row "Marks / 2.00" column of "quizattemptsummary" table should contain "0.00"
And I should see "Finished" in the "Attempt 1" "list_item"
And I should see "1.00/2.00" in the "Attempt 1" "list_item"
And I should see "Finished" in the "Attempt 2" "list_item"
And I should see "0.00/2.00" in the "Attempt 2" "list_item"

0 comments on commit a57d88b

Please sign in to comment.