Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions lang/en/assignsubmission_qpy.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,4 @@
$string['regradeall'] = 'Regrade all submissions (in background)';
$string['regradeall_help'] = 'If checked, all existing submissions for this assignment will be regraded using the selected question version. This process runs in the background.';
$string['submissionnotfound'] = 'No question submission found.';
$string['summaryresponsefiles'] = '{$a} files were part of the submission:';
$string['summaryresponsestring'] = '<code>{$a->key}</code> = <code>"{$a->value}"</code>';
$string['summarytoomanyitems'] = '{$a->responsefieldcount} response fields and {$a->filecount} files';
$string['summaryresponsestring'] = '<code>{$a->key}</code> = <code>{$a->value}</code>';
120 changes: 81 additions & 39 deletions locallib.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class assign_submission_qpy extends assign_submission_plugin {
private const RESPONSE_FILES_AREA = 'qpy_response_files';

/** @var int */
private const SUMMARY_MAX_ITEMS = 5;
private const COLLAPSIBLE_OPENED_THRESHOLD = 5;

/**
* Get the name of the qpy submission plugin.
Expand Down Expand Up @@ -505,60 +505,102 @@ public function save(stdClass $submission, stdClass $data) {
* @throws moodle_exception
*/
public function view_summary(stdClass $submission, &$showviewlink): string {
global $OUTPUT;

if (
!(str_ends_with($_SERVER['SCRIPT_NAME'], 'view.php') && optional_param('action', '', PARAM_ALPHANUMEXT) === 'grading')
) {
return $this->view($submission, false);
}

// The grading page has a long table. Do not display the full submission.
if (str_ends_with($_SERVER['SCRIPT_NAME'], 'view.php') && optional_param('action', '', PARAM_ALPHANUMEXT) === 'grading') {
$showviewlink = true; // Always show the link to view the attempt and history.
$quba = $this->get_question_usage($submission);
$attempt = $quba->get_question_attempt($quba->get_first_question_number());
$response = utils::get_qpy_response($attempt);

$fs = get_file_storage();
$files = $fs->get_area_files(
$this->assignment->get_context()->id,
'assignsubmission_qpy',
self::RESPONSE_FILES_AREA,
$submission->id,
includedirs: false
);
if ($response === null && !$files) {
return get_string('noqpyresponse', 'assignsubmission_qpy');
}
$showviewlink = true; // Always show the link to view the attempt and history.
$quba = $this->get_question_usage($submission);
$attempt = $quba->get_question_attempt($quba->get_first_question_number());
$response = utils::get_qpy_response($attempt);
$response = get_object_vars($response ?? (object) []);

if (isset($response->data) && !(array)$response->data) {
// Showing the dynamic JS data when it isn't used would just be confusing.
unset($response->data);
}
$fs = get_file_storage();
$files = $fs->get_area_files(
$this->assignment->get_context()->id,
'assignsubmission_qpy',
self::RESPONSE_FILES_AREA,
$submission->id,
includedirs: false
);

if (count($files) + count((array) $response) > self::SUMMARY_MAX_ITEMS) {
return get_string('summarytoomanyitems', 'assignsubmission_qpy', a: [
'responsefieldcount' => count((array) $response),
'filecount' => count($files),
]);
$dynamicdata = get_object_vars($response['data'] ?? (object) []);
unset($response['data']);

// Whether each collapsible should be open or closed.
$opened = count($response) + count($dynamicdata) + count($files) < self::COLLAPSIBLE_OPENED_THRESHOLD;

$html = '';

if ($files) {
$filelistitems = [];
foreach ($files as $file) {
$fileurl = moodle_url::make_pluginfile_url(
$file->get_contextid(),
$file->get_component(),
$file->get_filearea(),
$file->get_itemid(),
$file->get_filepath(),
$file->get_filename()
);
$linktext = $file->get_filename() . ' (' . display_size($file->get_filesize()) . ')';

$filelistitems[] = html_writer::link($fileurl, $linktext);
}
sort($filelistitems);

$title = get_string('response_summary_files', 'qtype_questionpy');
$html .= $OUTPUT->render_from_template('assignsubmission_qpy/collapsible', [
'title' => $title . ' (' . count($files) . ')',
'content' => html_writer::alist($filelistitems, ['class' => 'm-1']),
'open' => $opened,
]);
}

$html = '';
if ($response) {
ksort($response);

$listitems = [];
$responselistitems = [];
foreach ($response as $responsekey => $responsevalue) {
assert(is_string($responsevalue));
$listitems[] = get_string('summaryresponsestring', 'assignsubmission_qpy', [
$responselistitems[] = get_string('summaryresponsestring', 'assignsubmission_qpy', [
'key' => s($responsekey),
'value' => s($responsevalue),
]);
}
if ($listitems) {
$html .= html_writer::alist($listitems, ['class' => 'm-1 list-unstyled']);
}

if ($files) {
$html .= get_string('summaryresponsefiles', 'assignsubmission_qpy', a: count($files));
$html .= $this->assignment->render_area_files('assignsubmission_qpy', self::RESPONSE_FILES_AREA, $submission->id);
$title = get_string('response_summary_form_data', 'qtype_questionpy');
$html .= $OUTPUT->render_from_template('assignsubmission_qpy/collapsible', [
'title' => $title . ' (' . count($responselistitems) . ')',
'content' => html_writer::alist($responselistitems, ['class' => 'm-1 list-unstyled']),
'open' => $opened,
]);
}

if ($dynamicdata) {
ksort($dynamicdata);

$dynamicdatalistitems = [];
foreach ($dynamicdata as $dynamicdatakey => $dynamicdatavalue) {
$dynamicdatalistitems[] = get_string('summaryresponsestring', 'assignsubmission_qpy', [
'key' => s($dynamicdatakey),
'value' => s(json_encode($dynamicdatavalue)),
]);
}

return $html;
$title = get_string('response_summary_dynamic_data', 'qtype_questionpy');
$html .= $OUTPUT->render_from_template('assignsubmission_qpy/collapsible', [
'title' => $title . ' (' . count($dynamicdatalistitems) . ')',
'content' => html_writer::alist($dynamicdatalistitems, ['class' => 'm-1 list-unstyled']),
'open' => $opened,
]);
}

return $this->view($submission, false);
return $html ?: get_string('noqpyresponse', 'assignsubmission_qpy');
}

/**
Expand Down
47 changes: 47 additions & 0 deletions templates/collapsible.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{{!
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 assignsubmission_qpy/collapsable_section

Small collapsible section. Inspired by core/local/collapsable_section.

Optional blocks:
* title - the title content.
* content - the collapsible content.
* open - whether the collapsible section is open.

Example context (json):
{
"title": "Title",
"content": "Content",
"open": true
}
}}
<div>
<a data-bs-toggle="collapse"
href="#assignsubmission_qpy-collapse-{{uniqid}}"
role="button"
{{#open}} aria-expanded="true" {{/open}}
{{^open}} aria-expanded="false" {{/open}}
aria-controls="assignsubmission_qpy-collapse-{{uniqid}}">
{{title}}
</a>
<div class="collapse {{#open}}show{{/open}}" id="assignsubmission_qpy-collapse-{{uniqid}}">
{{{content}}}
</div>
</div>