From a57d88b3834806e5daa63b2d07d39d819b671d0e Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Fri, 9 Feb 2024 19:52:49 +0000 Subject: [PATCH] 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. --- .../classes/output/attempt_summary_information.php | 20 +++-- mod/quiz/classes/output/list_of_attempts.php | 85 ++++++++++++++++++++++ mod/quiz/classes/output/renderer.php | 4 +- mod/quiz/classes/output/view_page.php | 2 + mod/quiz/classes/quiz_attempt.php | 5 ++ mod/quiz/lang/en/quiz.php | 2 +- mod/quiz/styles.css | 18 ----- mod/quiz/templates/list_of_attempts.mustache | 68 +++++++++++++++++ mod/quiz/tests/attempt_test.php | 52 ------------- mod/quiz/tests/behat/preview.feature | 4 +- mod/quiz/tests/behat/reattempt_quiz.feature | 6 +- mod/quiz/upgrade.txt | 1 + mod/quiz/view.php | 5 ++ 13 files changed, 190 insertions(+), 82 deletions(-) create mode 100644 mod/quiz/classes/output/list_of_attempts.php create mode 100644 mod/quiz/templates/list_of_attempts.mustache diff --git a/mod/quiz/classes/output/attempt_summary_information.php b/mod/quiz/classes/output/attempt_summary_information.php index 20ddd630e95..6ca0a36d94e 100644 --- a/mod/quiz/classes/output/attempt_summary_information.php +++ b/mod/quiz/classes/output/attempt_summary_information.php @@ -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'; + } } diff --git a/mod/quiz/classes/output/list_of_attempts.php b/mod/quiz/classes/output/list_of_attempts.php new file mode 100644 index 00000000000..c1ae66e14e5 --- /dev/null +++ b/mod/quiz/classes/output/list_of_attempts.php @@ -0,0 +1,85 @@ +. + +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'; + } +} diff --git a/mod/quiz/classes/output/renderer.php b/mod/quiz/classes/output/renderer.php index 9e9b5d4be2e..070b8e330ee 100644 --- a/mod/quiz/classes/output/renderer.php +++ b/mod/quiz/classes/output/renderer.php @@ -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 ''; } diff --git a/mod/quiz/classes/output/view_page.php b/mod/quiz/classes/output/view_page.php index bc967837efa..a1e9ed55b39 100644 --- a/mod/quiz/classes/output/view_page.php +++ b/mod/quiz/classes/output/view_page.php @@ -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 diff --git a/mod/quiz/classes/quiz_attempt.php b/mod/quiz/classes/quiz_attempt.php index 1ec18b5522a..d6b5479437a 100644 --- a/mod/quiz/classes/quiz_attempt.php +++ b/mod/quiz/classes/quiz_attempt.php @@ -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); } diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index b18d7349bed..df4f3ef522e 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -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.
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.'; diff --git a/mod/quiz/styles.css b/mod/quiz/styles.css index 18da3255922..12853b40a4b 100644 --- a/mod/quiz/styles.css +++ b/mod/quiz/styles.css @@ -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; diff --git a/mod/quiz/templates/list_of_attempts.mustache b/mod/quiz/templates/list_of_attempts.mustache new file mode 100644 index 00000000000..17ccfab225b --- /dev/null +++ b/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 . +}} +{{! + @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": "Review", + "summarydata": { + "hasitems": true, + "items": [ + { + "title": "It is me!", + "content": "Sam Student" + }, + {"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}} +

{{# str}}summaryofattempts, quiz{{/str}}

+ + + +{{/hasattempts}} diff --git a/mod/quiz/tests/attempt_test.php b/mod/quiz/tests/attempt_test.php index 73733506608..154b288f91f 100644 --- a/mod/quiz/tests/attempt_test.php +++ b/mod/quiz/tests/attempt_test.php @@ -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 = '/]*>' . get_string('summaryofattempts', 'quiz') . '<\/caption>/'; - - // Check caption existed. - $this->assertMatchesRegularExpression($captionpattern, $table); - // Check column attempt. - $this->assertMatchesRegularExpression('/]*>' . $attempt->get_attempt_number() . '<\/td>/', $table); - // Check column state. - $this->assertMatchesRegularExpression('/]*>' . ucfirst($attempt->get_state()) . '.+?<\/td>/', $table); - // Check column marks. - $this->assertMatchesRegularExpression('/]* c2.+?' . - quiz_format_grade($quiz, $attempt->get_sum_marks()) .'<\/td>/', $table); - // Check column grades. - $this->assertMatchesRegularExpression('/]* c2.+?0\.00<\/td>/', $table); - // Check column review. - $this->assertMatchesRegularExpression('/]*>.+?Review<\/a><\/td>/', $table); - } } diff --git a/mod/quiz/tests/behat/preview.feature b/mod/quiz/tests/behat/preview.feature index a569ce362e6..c2720095576 100644 --- a/mod/quiz/tests/behat/preview.feature +++ b/mod/quiz/tests/behat/preview.feature @@ -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" diff --git a/mod/quiz/tests/behat/reattempt_quiz.feature b/mod/quiz/tests/behat/reattempt_quiz.feature index 2680b62ea9c..36cd2b3bcc9 100644 --- a/mod/quiz/tests/behat/reattempt_quiz.feature +++ b/mod/quiz/tests/behat/reattempt_quiz.feature @@ -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" diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index daf85afdb22..b0892c03ad5 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -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 === diff --git a/mod/quiz/view.php b/mod/quiz/view.php index 1bf80a45985..b4bc3b89fab 100644 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -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) { -- 2.11.4.GIT