MDL-80880 quiz: change display of previous attempts summary

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.
This commit is contained in:
Tim Hunt
2024-03-05 12:02:11 +00:00
parent 8feeacc425
commit a57d88b383
13 changed files with 190 additions and 82 deletions
@@ -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;
@@ -25,7 +26,6 @@ use question_display_options;
use renderable;
use renderer_base;
use stdClass;
use templatable;
use user_picture;
/**
@@ -34,12 +34,13 @@ use user_picture;
* 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 = [];
@@ -90,7 +91,8 @@ class attempt_summary_information implements renderable, templatable {
*
* @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.
@@ -99,7 +101,7 @@ class attempt_summary_information implements renderable, templatable {
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;
@@ -119,9 +121,9 @@ class attempt_summary_information implements renderable, templatable {
);
}
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);
}
@@ -239,4 +241,10 @@ class attempt_summary_information implements renderable, templatable {
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';
}
}
@@ -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';
}
}
+3 -1
View File
@@ -889,8 +889,8 @@ class renderer extends plugin_renderer_base {
$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;
}
@@ -1115,8 +1115,10 @@ class renderer extends plugin_renderer_base {
* @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 '';
}
+2
View File
@@ -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
+5
View File
@@ -593,10 +593,15 @@ class quiz_attempt {
* 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);
}
+1 -1
View File
@@ -1016,7 +1016,7 @@ $string['subplugintype_quizaccess'] = 'Access rule';
$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.';
-18
View File
@@ -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;
@@ -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
View File
@@ -507,56 +507,4 @@ class attempt_test extends \advanced_testcase {
$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);
}
}
+2 -2
View File
@@ -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
@@ -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"
+4 -2
View File
@@ -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"
+1
View File
@@ -15,6 +15,7 @@ This file describes API changes in the quiz code.
replaced by review_attempt_summary and filter_review_attempt_summary. This is to support changing the $summarydata argument
the review_page() method to an instance of a new templateable class attempt_summary_information for displaying this.
The $summarydata argument of review_question_page has also been changed to an attempt_summary_information.
* In the renderer, the view_table has been deprecated. Please use the list_of_attempts renderable instead.
=== 4.3 ===
+5
View File
@@ -24,6 +24,7 @@
*/
use mod_quiz\access_manager;
use mod_quiz\output\list_of_attempts;
use mod_quiz\output\renderer;
use mod_quiz\output\view_page;
use mod_quiz\quiz_attempt;
@@ -100,6 +101,10 @@ $viewobj->attemptobjs = [];
foreach ($attempts as $attempt) {
$viewobj->attemptobjs[] = new quiz_attempt($attempt, $quiz, $cm, $course, false);
}
$viewobj->attemptslist = new list_of_attempts($timenow);
foreach (array_reverse($viewobj->attemptobjs) as $attemptobj) {
$viewobj->attemptslist->add_attempt($attemptobj);
}
// Work out the final grade, checking whether it was overridden in the gradebook.
if (!$canpreview) {