MDL-84387 mod_assign: Improve submission confirmation with file list

This add the ability for assingment submission plugins to summarise
what was submitted, and then that summary is included in the
message to students confirming to them that their submission
was received.

Then, this new API is implemented in the file and online text plugins.

Co-authored-by: Tim Hunt <[email protected]>
This commit is contained in:
toanlamt
2025-03-19 20:08:02 +00:00
committed by Tim Hunt
co-authored by Tim Hunt
parent c64e967ce6
commit 4898bee134
13 changed files with 435 additions and 17 deletions
@@ -0,0 +1,8 @@
issueNumber: MDL-84387
notes:
mod_assign:
- message: >-
There is a new method `submission_summary_for_messages()` for submission
sub-plugins to summarise what has been submitted for inclusion in
confirmation messages to students.
type: improved
+13 -11
View File
@@ -571,8 +571,8 @@ $string['submissionactions'] = 'Submission actions';
$string['submissionattachments'] = 'Only show files during submission';
$string['submissionattachments_help'] = 'Tick the box to only show files on the submission page. Otherwise, files will be shown on both the assignment and submission pages.';
$string['confirmstart'] = 'You have {$a} to complete this assignment. When you begin, the timer will start to count down and can\'t be paused.';
$string['submissioncopiedtext'] = 'You have made a copy of your previous
assignment submission for \'{$a->assignment}\'
$string['submissioncontains'] = 'Your submission contains:';
$string['submissioncopiedtext'] = 'You have made a copy of your previous assignment submission for \'{$a->assignment}\'
You can see the status of your assignment submission:
@@ -591,9 +591,11 @@ $string['submissionnotopen'] = 'This assignment is not open for submissions';
$string['submissionnotready'] = 'This assignment is not ready to submit:';
$string['privacy:submissionpath'] = 'submission';
$string['submissionplugins'] = 'Submission plugins';
$string['submissionreceipts'] = 'Send submission receipts';
$string['submissionreceiptothertext'] = 'Your assignment submission for
\'{$a->assignment}\' has been submitted.
$string['submissionreceiptcontains'] = 'Submitted content ({$a->total} items):';
$string['submissionreceipthtml'] = '<p>Your assignment for <strong>{$a->assignment}</strong> has been successfully submitted.</p>
<p>You can view your submission and check its status on the <a href="{$a->url}">assignment page</a>.</p>
{$a->submissionsummaryhtml}';
$string['submissionreceiptothertext'] = 'Your assignment submission for \'{$a->assignment}\' has been submitted.
You can see the status of your assignment submission:
@@ -602,15 +604,15 @@ $string['submissionreceiptotherhtml'] = 'Your assignment submission for
\'<i>{$a->assignment}</i>\' has been submitted.<br /><br />
You can see the status of your <a href="{$a->url}">assignment submission</a>.';
$string['submissionreceiptothersmall'] = 'Your assignment submission for {$a->assignment} has been submitted.';
$string['submissionreceipttext'] = 'You have submitted an
assignment submission for \'{$a->assignment}\'
$string['submissionreceipts'] = 'Send submission receipts';
$string['submissionreceiptsmall'] = 'Assignment Submission Confirmation - {$a->assignment}';
$string['submissionreceipttext'] = 'You have submitted an assignment submission for \'{$a->assignment}\'.
You can see the status of your assignment submission:
{$a->url}';
$string['submissionreceipthtml'] = '<p>You have submitted an assignment submission for \'<i>{$a->assignment}</i>\'.</p>
<p>You can see the status of your <a href="{$a->url}">assignment submission</a>.</p>';
$string['submissionreceiptsmall'] = 'You have submitted your assignment submission for {$a->assignment}';
{$a->url}
{$a->submissionsummarytext}';
$string['submissions'] = 'Submissions';
$string['submissionslocked'] = 'This assignment is not accepting submissions';
$string['submissionslockedshort'] = 'Submission changes not allowed';
+59 -5
View File
@@ -6465,6 +6465,7 @@ class assign {
* @param string $assignmentname
* @param bool $blindmarking
* @param int $uniqueidforuser
* @param array $extrainfo extra values to pass to any language strings or templates used in preparing the message.
* @return void
*/
public static function send_assignment_notification($userfrom,
@@ -6478,7 +6479,8 @@ class assign {
$modulename,
$assignmentname,
$blindmarking,
$uniqueidforuser) {
$uniqueidforuser,
$extrainfo = []) {
global $CFG, $PAGE;
$info = new stdClass();
@@ -6494,6 +6496,7 @@ class assign {
$info->assignment = format_string($assignmentname, true, array('context'=>$context));
$info->url = $CFG->wwwroot.'/mod/assign/view.php?id='.$coursemodule->id;
$info->timeupdated = userdate($updatetime, get_string('strftimerecentfull'));
$info = (object) array_merge((array) $info, $extrainfo);
$postsubject = get_string($messagetype . 'small', 'assign', $info);
$posttext = self::format_notification_message_text($messagetype,
@@ -6556,9 +6559,10 @@ class assign {
* @param string $messagetype
* @param string $eventtype
* @param int $updatetime
* @param array $extrainfo extra values to pass to any language strings or templates used in preparing the message.
* @return void
*/
public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime) {
public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime, $extrainfo = []) {
global $USER;
$userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
$uniqueid = $this->get_uniqueid_for_user($userid);
@@ -6573,7 +6577,8 @@ class assign {
$this->get_module_name(),
$this->get_instance()->name,
$this->is_blind_marking(),
$uniqueid);
$uniqueid,
$extrainfo);
}
/**
@@ -6621,21 +6626,70 @@ class assign {
} else {
$user = $USER;
}
// Prepare extra data for submission receipt notification.
$extrainfo = $this->get_submission_summaries_for_messages($submission);
if ($submission->userid == $USER->id) {
$this->send_notification(core_user::get_noreply_user(),
$user,
'submissionreceipt',
'assign_notification',
$submission->timemodified);
$submission->timemodified,
$extrainfo);
} else {
$this->send_notification($USER,
$user,
'submissionreceiptother',
'assign_notification',
$submission->timemodified);
$submission->timemodified,
$extrainfo);
}
}
/**
* Produce a summary of a submission that can be used in messages.
*
* This function iterates through all enabled submission plugins and calls their
* `get_submission_summary` method (if implemented). It aggregates the results
* into a formatted summary string.
*
* @param stdClass $submission the submission the message is about. Row from assign_submission table.
* @return string[] with two elements:
* 'submissionsummarytext' => a plain text summary,
* 'submissionsummaryhtml' => an HTML summary.
*/
protected function get_submission_summaries_for_messages(stdClass $submission): array {
$textsummaries = [];
$htmlsummaries = [];
foreach ($this->submissionplugins as $plugin) {
if ($plugin->is_enabled() && $plugin->is_visible()) {
[$textsummary, $htmlsummary] = $plugin->submission_summary_for_messages($submission);
if ($textsummary) {
$textsummaries[] = $textsummary;
}
if ($htmlsummary) {
$htmlsummaries[] = $htmlsummary;
}
}
}
$textsummary = '';
if ($textsummaries) {
$textsummary = get_string('submissioncontains', 'assign') . "\n\n" .
implode("\n", $textsummaries);
}
$htmlsummary = '';
if ($htmlsummaries) {
$htmlsummary = html_writer::tag('h2', get_string('submissioncontains', 'assign')) .
implode('', $htmlsummaries);
}
return [
'submissionsummarytext' => $textsummary,
'submissionsummaryhtml' => $htmlsummary,
];
}
/**
* Send notifications to graders upon student submissions.
*
+3 -1
View File
@@ -701,7 +701,7 @@ class assign_course_index_summary implements renderable {
class assign_files implements renderable {
/** @var context $context */
public $context;
/** @var string $context */
/** @var array $dir as returned by {@see file_storage::get_area_tree()}. */
public $dir;
/** @var MoodleQuickForm $portfolioform */
public $portfolioform;
@@ -799,4 +799,6 @@ class assign_files implements renderable {
true,
);
}
}
@@ -34,6 +34,7 @@ $string['enabled'] = 'File submissions';
$string['enabled_help'] = 'If enabled, students are able to upload one or more files as their submission.';
$string['eventassessableuploaded'] = 'A file has been uploaded.';
$string['file'] = 'File submissions';
$string['filewithsize'] = '{$a->filename} ({$a->size})';
$string['maxbytes'] = 'Maximum file size';
$string['maxfiles'] = 'Maximum files per submission';
$string['maxfiles_help'] = 'If file submissions are enabled, each assignment can be set to accept up to this number of files for their submission.';
+57
View File
@@ -381,6 +381,63 @@ class assign_submission_file extends assign_submission_plugin {
}
}
#[\Override]
public function submission_summary_for_messages(stdClass $submission): array {
global $PAGE;
$files = new assign_files($this->assignment->get_context(), $submission->id,
ASSIGNSUBMISSION_FILE_FILEAREA, 'assignsubmission_file', $this->assignment->get_course(),
$this->assignment->get_course_module());
$filelist = $this->flatten_list_of_files($files->dir);
if (!$filelist) {
return ['', ''];
}
$renderer = $PAGE->get_renderer('mod_assign');
$templatecontext = ['files' => $filelist];
return [
// Mustache strips off all trailing whitespace, but we want a newline at the end.
$renderer->render_from_template(
'assignsubmission_file/email_summary_text', $templatecontext) . "\n",
$renderer->render_from_template(
'assignsubmission_file/email_summary_html', $templatecontext),
];
}
/**
* Turn the hierarchical list of files into a flat array.
*
* The returned array looks like
* [
* ['filepath' => 'File 1.docx', 'filesize' => '12.3KB'],
* ['filepath' => 'subdir/extra data.txt, 'filesize' => '456B'],
* ].
*
* @param array $dir as returned by {@see file_storage::get_area_tree()}.
* @return array as above.
*/
private function flatten_list_of_files(array $dir): array {
if (empty($dir['subdirs']) && empty($dir['files'])) {
return [];
}
$filessummary = [];
/** @var stored_file $file */
foreach ($dir['files'] as $file) {
$filessummary[] = [
'filepath' => ltrim($file->get_filepath(), '/') . $file->get_filename(),
'filesize' => display_size($file->get_filesize()),
];
}
foreach ($dir['subdirs'] as $subdir) {
$files = $this->format_submission_files($subdir);
$filessummary = array_merge($filessummary, $files);
}
return $filessummary;
}
/**
* No full submission view - the summary contains the list of files and that is the whole submission
*
@@ -0,0 +1,48 @@
{{!
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_file/email_summary_html
This template is used render the HTML version of the summary of files submissions to go into emails.
Classes required for JS:
* none
Context variables required for this template:
* files - array of sumbitted files, each with properties filepath and filesize.
Example context (json):
{
"files": [
{
"filepath": "File 1.docx",
"filesize": "12.3KB"
},
{
"filepath": "subdir/extra data.txt",
"filesize": "456B"
}
]
}
}}
<h3>{{#str}}file, assignsubmission_file{{/str}}</h3>
<ul>
{{#files}}
<li>{{#str}}filewithsize, assignsubmission_file, {"filename": {{#quote}}{{filepath}}{{/quote}}, "size": {{#quote}}{{filesize}}{{/quote}}, "coursename": {{#quote}}{{coursename}}{{/quote}} } {{/str}}</li>
{{/files}}
</ul>
@@ -0,0 +1,45 @@
{{!
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_file/email_summary_text
This template is used render the plain text version of the summary of files submissions to go into emails.
Classes required for JS:
* none
Context variables required for this template:
* files - array of sumbitted files, each with properties filepath and filesize.
Example context (json):
{
"files": [
{
"filepath": "File 1.docx",
"filesize": "12.3KB"
},
{
"filepath": "subdir/extra data.txt",
"filesize": "456B"
}
]
}
}}
{{#str}}file, assignsubmission_file{{/str}}
{{#files}}
* {{#str}}filewithsize, assignsubmission_file, {"filename": {{#quote}}{{filepath}}{{/quote}}, "size": {{#quote}}{{filesize}}{{/quote}}, "coursename": {{#quote}}{{coursename}}{{/quote}} } {{/str}}
{{/files}}
@@ -407,6 +407,27 @@ class assign_submission_onlinetext extends assign_submission_plugin {
return '';
}
#[\Override]
public function submission_summary_for_messages(stdClass $submission): array {
global $PAGE;
$onlinetextsubmission = $this->get_onlinetext_submission($submission->id);
if (!$onlinetextsubmission || !$onlinetextsubmission->onlinetext) {
return ['', ''];
}
$renderer = $PAGE->get_renderer('mod_assign');
$templatecontext = ['wordcount' => count_words($onlinetextsubmission->onlinetext)];
return [
// Mustache strips off all trailing whitespace, but we want a newline at the end.
$renderer->render_from_template(
'assignsubmission_onlinetext/email_summary_text', $templatecontext) . "\n",
$renderer->render_from_template(
'assignsubmission_onlinetext/email_summary_html', $templatecontext),
];
}
/**
* Produce a list of files suitable for export that represent this submission.
*
@@ -0,0 +1,34 @@
{{!
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_onlinetext/email_summary_html
This template is used render the HTML version of the summary of online text submissions to go into emails.
Classes required for JS:
* none
Context variables required for this template:
* wordcount - Word count of the student's submission.
Example context (json):
{
"wordcount": 123
}
}}
<h3>{{#str}}onlinetext, assignsubmission_onlinetext{{/str}}</h3>
<p>{{#str}}numwords, assignsubmission_onlinetext, {{wordcount}}{{/str}}</p>
@@ -0,0 +1,34 @@
{{!
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_onlinetext/email_summary_text
This template is used render the plain text version of the summary of online text submissions to go into emails.
Classes required for JS:
* none
Context variables required for this template:
* wordcount - Word count of the student's submission.
Example context (json):
{
"wordcount": 123
}
}}
{{#str}}onlinetext, assignsubmission_onlinetext{{/str}}
{{#str}}numwords, assignsubmission_onlinetext, {{wordcount}}{{/str}}
+18
View File
@@ -154,4 +154,22 @@ abstract class assign_submission_plugin extends assign_plugin {
public function allow_image_conversion() {
return false;
}
/**
* Summarise a submission for inclusion in messages.
*
* Moodle messages can be sent as either HTML or plain text, so you need to
* produce two versions of the summary.
*
* If there is nothing in the submission from your plugin return an array of two empty strings.
*
* The plain text version should finish in a newline character.
* The HTML version should have block-level elements like headings or <p>s as the outer elements.
*
* @param stdClass $submission the assign_submission record for the submission the message is about.
* @return string[] with two elements, a plain text summary and an HTML summary.
*/
public function submission_summary_for_messages(stdClass $submission): array {
return ['', ''];
}
}
@@ -782,4 +782,98 @@ final class notification_helper_test extends \advanced_testcase {
// Clear sink.
$sink->clear();
}
/**
* Test sending the assignment notification to a user with a list of the submitted files.
*/
public function test_send_notification_with_summary_to_user(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$sink = $this->redirectMessages();
// Create a course and enrol a user.
$course = $generator->create_course(['shortname' => 'A100']);
$user1 = $generator->create_user();
$generator->enrol_user($user1->id, $course->id, 'student');
/** @var \mod_assign_generator $assignmentgenerator */
$assignmentgenerator = $generator->get_plugin_generator('mod_assign');
// Create activity.
$assignment = $assignmentgenerator->create_instance([
'course' => $course->id,
'name' => 'Assignment 1',
'submissiondrafts' => 0,
'assignsubmission_file_enabled' => 1,
'assignsubmission_file_maxfiles' => 12,
'assignsubmission_file_maxsizebytes' => 1024 * 1024,
'assignsubmission_onlinetext_enabled' => 1,
]);
$filename1 = 'submissionsample01.txt';
$filename2 = 'submissionsample02.txt';
$files = [
"mod/assign/tests/fixtures/" . $filename1,
"mod/assign/tests/fixtures/" . $filename2,
];
// Generate submissions.
$assignmentgenerator->create_submission([
'userid' => $user1->id,
'cmid' => $assignment->cmid,
'status' => 'submitted',
'file' => implode(',', $files),
'onlinetext' => 'Some text example',
]);
// Get the notifications.
$messages = $sink->get_messages_by_component('mod_assign');
$this->assertCount(1, $messages);
$message = reset($messages);
// Check the subject line and short message.
$this->assertEquals('Assignment Submission Confirmation - Assignment 1', $message->subject);
$this->assertEquals('Assignment Submission Confirmation - Assignment 1', $message->smallmessage);
// Check the plain text message.
$this->assertEquals('A100 -> Assignment -> Assignment 1
---------------------------------------------------------------------
You have submitted an assignment submission for \'Assignment 1\'.
You can see the status of your assignment submission:
https://www.example.com/moodle/mod/assign/view.php?id=' . $assignment->cmid . '
Your submission contains:
Online text
(3 words)
File submissions
* submissionsample01.txt (42 bytes)
* submissionsample02.txt (42 bytes)
---------------------------------------------------------------------
', $message->fullmessage);
$expectedfragments = [
'<p>Your assignment for <strong>Assignment 1</strong> has been successfully submitted.</p>',
'<p>You can view your submission and check its status on the <a href="' .
'https://www.example.com/moodle/mod/assign/view.php?id=' .
$assignment->cmid . '">assignment page</a>.</p>',
'<h2>Your submission contains:</h2>',
'<h3>Online text</h3>',
'<p>(3 words)</p>',
'<h3>File submissions</h3>',
'<li>submissionsample01.txt (42 bytes)</li>',
'<li>submissionsample02.txt (42 bytes)</li>',
];
foreach ($expectedfragments as $html) {
$this->assertStringContainsString($html, $message->fullmessagehtml);
}
// Clear sink.
$sink->clear();
}
}