Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 0 additions & 2 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';
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:
* titlecontent - the collpasible title content.
* sectioncontent - 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>