2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 namespace mod_quiz\output
;
20 use core\output\named_templatable
;
22 use mod_quiz\quiz_attempt
;
24 use mod_quiz\question\display_options
;
25 use question_display_options
;
32 * A summary of a single quiz attempt for rendering.
34 * This is used in places like
35 * - at the top of the review attempt page (review.php)
36 * - at the top of the review single question page (reviewquestion.php)
37 * - on the quiz entry page (view.php).
40 * @copyright 2024 The Open University
41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43 class attempt_summary_information
implements renderable
, named_templatable
{
45 /** @var array[] The rows of summary data. {@see add_item()} should make the structure clear. */
46 protected array $summarydata = [];
49 * Add an item to the summary.
51 * @param string $shortname unique identifier of this item (not displayed).
52 * @param string|renderable $title the title of this item.
53 * @param string|renderable $content the content of this item.
55 public function add_item(string $shortname, string|renderable
$title, string|renderable
$content): void
{
56 $this->summarydata
[$shortname] = [
58 'content' => $content,
63 * Filter the data held, to keep only the information with the given shortnames.
65 * @param array $shortnames items to keep.
67 public function filter_keeping_only(array $shortnames): void
{
68 foreach ($this->summarydata
as $shortname => $rowdata) {
69 if (!in_array($shortname, $shortnames)) {
70 unset($this->summarydata
[$shortname]);
76 * To aid conversion of old code. This converts the old array format into an instance of this class.
78 * @param array $items array of $shortname => [$title, $content].
81 public static function create_from_legacy_array(array $items): static {
82 $summary = new static();
83 foreach ($items as $shortname => $item) {
84 $summary->add_item($shortname, $item['title'], $item['content']);
90 * Initialise an instance of this class for a particular quiz attempt.
92 * @param quiz_attempt $attemptobj the attempt to summarise.
93 * @param display_options $options options for what can be seen.
94 * @param int|null $pageforlinkingtootherattempts if null, no links to other attempsts will be created.
95 * If specified, the URL of this particular page of the attempt, otherwise
96 * the URL will go to the first page. If -1, deduce $page from $slot.
97 * @param bool|null $showall if true, the URL will be to review the entire attempt on one page,
98 * and $page will be ignored. If null, a sensible default will be chosen.
99 * @return self summary information.
101 public static function create_for_attempt(
102 quiz_attempt
$attemptobj,
103 display_options
$options,
104 ?
int $pageforlinkingtootherattempts = null,
105 ?
bool $showall = null,
108 $summary = new static();
110 // Prepare summary information about the whole attempt.
111 if (!$attemptobj->get_quiz()->showuserpicture
&& $attemptobj->get_userid() != $USER->id
) {
112 // If showuserpicture is true, the picture is shown elsewhere, so don't repeat it.
113 $student = $DB->get_record('user', ['id' => $attemptobj->get_userid()]);
114 $userpicture = new user_picture($student);
115 $userpicture->courseid
= $attemptobj->get_courseid();
116 $summary->add_item('user', $userpicture,
118 new moodle_url('/user/view.php', ['id' => $student->id
, 'course' => $attemptobj->get_courseid()]),
119 fullname($student, true),
124 if ($pageforlinkingtootherattempts !== null && $attemptobj->has_capability('mod/quiz:viewreports')) {
125 $attemptlist = $attemptobj->links_to_other_attempts(
126 $attemptobj->review_url(null, $pageforlinkingtootherattempts, $showall));
128 $summary->add_item('attemptlist', get_string('attempts', 'quiz'), $attemptlist);
132 // Timing information.
133 $attempt = $attemptobj->get_attempt();
134 $quiz = $attemptobj->get_quiz();
137 if ($attempt->state
== quiz_attempt
::FINISHED
) {
138 if ($timetaken = ($attempt->timefinish
- $attempt->timestart
)) {
139 if ($quiz->timelimit
&& $timetaken > ($quiz->timelimit +
60)) {
140 $overtime = $timetaken - $quiz->timelimit
;
141 $overtime = format_time($overtime);
143 $timetaken = format_time($timetaken);
148 $timetaken = get_string('unfinished', 'quiz');
151 $summary->add_item('startedon', get_string('startedon', 'quiz'), userdate($attempt->timestart
));
153 $summary->add_item('state', get_string('attemptstate', 'quiz'),
154 quiz_attempt
::state_name($attemptobj->get_attempt()->state
));
156 if ($attempt->state
== quiz_attempt
::FINISHED
) {
157 $summary->add_item('completedon', get_string('completedon', 'quiz'),
158 userdate($attempt->timefinish
));
159 $summary->add_item('timetaken', get_string('timetaken', 'quiz'), $timetaken);
162 if (!empty($overtime)) {
163 $summary->add_item('overdue', get_string('overdue', 'quiz'), $overtime);
166 // Show marks (if the user is allowed to see marks at the moment).
167 $grade = quiz_rescale_grade($attempt->sumgrades
, $quiz, false);
168 if ($options->marks
>= question_display_options
::MARK_AND_MAX
&& quiz_has_grades($quiz)) {
170 if ($attempt->state
!= quiz_attempt
::FINISHED
) {
171 // Cannot display grade.
173 } else if (is_null($grade)) {
174 $summary->add_item('grade', get_string('gradenoun'),
175 quiz_format_grade($quiz, $grade));
178 // Show raw marks only if they are different from the grade (like on the view page).
179 if ($quiz->grade
!= $quiz->sumgrades
) {
181 $a->grade
= quiz_format_grade($quiz, $attempt->sumgrades
);
182 $a->maxgrade
= quiz_format_grade($quiz, $quiz->sumgrades
);
183 $summary->add_item('marks', get_string('marks', 'quiz'),
184 get_string('outofshort', 'quiz', $a));
187 // Now the scaled grade.
189 $a->grade
= html_writer
::tag('b', quiz_format_grade($quiz, $grade));
190 $a->maxgrade
= quiz_format_grade($quiz, $quiz->grade
);
191 if ($quiz->grade
!= 100) {
192 // Show the percentage using the configured number of decimal places,
193 // but without trailing zeroes.
194 $a->percent
= html_writer
::tag('b', format_float(
195 $attempt->sumgrades
* 100 / $quiz->sumgrades
,
196 $quiz->decimalpoints
, true, true));
197 $formattedgrade = get_string('outofpercent', 'quiz', $a);
199 $formattedgrade = get_string('outof', 'quiz', $a);
201 $summary->add_item('grade', get_string('gradenoun'),
206 // Any additional summary data from the behaviour.
207 foreach ($attemptobj->get_additional_summary_data($options) as $shortname => $data) {
208 $summary->add_item($shortname, $data['title'], $data['content']);
211 // Feedback if there is any, and the user is allowed to see it now.
212 $feedback = $attemptobj->get_overall_feedback($grade);
213 if ($options->overallfeedback
&& $feedback) {
214 $summary->add_item('feedback', get_string('feedback', 'quiz'), $feedback);
220 public function export_for_template(renderer_base
$output): array {
223 'hasitems' => !empty($this->summarydata
),
226 foreach ($this->summarydata
as $item) {
227 if ($item['title'] instanceof renderable
) {
228 $title = $output->render($item['title']);
230 $title = $item['title'];
233 if ($item['content'] instanceof renderable
) {
234 $content = $output->render($item['content']);
236 $content = $item['content'];
239 $templatecontext['items'][] = (object) ['title' => $title, 'content' => $content];
242 return $templatecontext;
245 public function get_template_name(\renderer_base
$renderer): string {
246 // Only reason we are forced to implement this is that we want the quiz renderer
247 // passed to export_for_template, not a core_renderer.
248 return 'mod_quiz/attempt_summary_information';