From 4898bee13413049655a87c473cd51ffbda7983bb Mon Sep 17 00:00:00 2001 From: toanlamt Date: Mon, 17 Feb 2025 13:46:03 +0700 Subject: [PATCH] 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 --- .upgradenotes/MDL-84387-2025031106322077.yml | 8 ++ mod/assign/lang/en/assign.php | 24 ++--- mod/assign/locallib.php | 64 ++++++++++++- mod/assign/renderable.php | 4 +- .../file/lang/en/assignsubmission_file.php | 1 + mod/assign/submission/file/locallib.php | 57 +++++++++++ .../templates/email_summary_html.mustache | 48 ++++++++++ .../templates/email_summary_text.mustache | 45 +++++++++ mod/assign/submission/onlinetext/locallib.php | 21 +++++ .../templates/email_summary_html.mustache | 34 +++++++ .../templates/email_summary_text.mustache | 34 +++++++ mod/assign/submissionplugin.php | 18 ++++ mod/assign/tests/notification_helper_test.php | 94 +++++++++++++++++++ 13 files changed, 435 insertions(+), 17 deletions(-) create mode 100644 .upgradenotes/MDL-84387-2025031106322077.yml create mode 100644 mod/assign/submission/file/templates/email_summary_html.mustache create mode 100644 mod/assign/submission/file/templates/email_summary_text.mustache create mode 100644 mod/assign/submission/onlinetext/templates/email_summary_html.mustache create mode 100644 mod/assign/submission/onlinetext/templates/email_summary_text.mustache diff --git a/.upgradenotes/MDL-84387-2025031106322077.yml b/.upgradenotes/MDL-84387-2025031106322077.yml new file mode 100644 index 00000000000..e2dc418c1bd --- /dev/null +++ b/.upgradenotes/MDL-84387-2025031106322077.yml @@ -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 diff --git a/mod/assign/lang/en/assign.php b/mod/assign/lang/en/assign.php index 8943de320af..279c7e860e2 100644 --- a/mod/assign/lang/en/assign.php +++ b/mod/assign/lang/en/assign.php @@ -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'] = '

Your assignment for {$a->assignment} has been successfully submitted.

+

You can view your submission and check its status on the assignment page.

+{$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 \'{$a->assignment}\' has been submitted.

You can see the status of your assignment submission.'; $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'] = '

You have submitted an assignment submission for \'{$a->assignment}\'.

-

You can see the status of your assignment submission.

'; -$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'; diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index d3a1674388d..f1caa4ce87b 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -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. * diff --git a/mod/assign/renderable.php b/mod/assign/renderable.php index c085225b88f..976b92c00a1 100644 --- a/mod/assign/renderable.php +++ b/mod/assign/renderable.php @@ -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, ); } + + } diff --git a/mod/assign/submission/file/lang/en/assignsubmission_file.php b/mod/assign/submission/file/lang/en/assignsubmission_file.php index 0cda3f23605..374df815066 100644 --- a/mod/assign/submission/file/lang/en/assignsubmission_file.php +++ b/mod/assign/submission/file/lang/en/assignsubmission_file.php @@ -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.'; diff --git a/mod/assign/submission/file/locallib.php b/mod/assign/submission/file/locallib.php index 71db60b96da..35c39f509a1 100644 --- a/mod/assign/submission/file/locallib.php +++ b/mod/assign/submission/file/locallib.php @@ -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 * diff --git a/mod/assign/submission/file/templates/email_summary_html.mustache b/mod/assign/submission/file/templates/email_summary_html.mustache new file mode 100644 index 00000000000..2c04a703e94 --- /dev/null +++ b/mod/assign/submission/file/templates/email_summary_html.mustache @@ -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 . +}} +{{! + @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" + } + ] + } +}} +

{{#str}}file, assignsubmission_file{{/str}}

+ +
    +{{#files}} +
  • {{#str}}filewithsize, assignsubmission_file, {"filename": {{#quote}}{{filepath}}{{/quote}}, "size": {{#quote}}{{filesize}}{{/quote}}, "coursename": {{#quote}}{{coursename}}{{/quote}} } {{/str}}
  • +{{/files}} +
diff --git a/mod/assign/submission/file/templates/email_summary_text.mustache b/mod/assign/submission/file/templates/email_summary_text.mustache new file mode 100644 index 00000000000..70d085a149a --- /dev/null +++ b/mod/assign/submission/file/templates/email_summary_text.mustache @@ -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 . +}} +{{! + @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}} diff --git a/mod/assign/submission/onlinetext/locallib.php b/mod/assign/submission/onlinetext/locallib.php index 2f0adc76f3f..2a35a9409c1 100644 --- a/mod/assign/submission/onlinetext/locallib.php +++ b/mod/assign/submission/onlinetext/locallib.php @@ -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. * diff --git a/mod/assign/submission/onlinetext/templates/email_summary_html.mustache b/mod/assign/submission/onlinetext/templates/email_summary_html.mustache new file mode 100644 index 00000000000..f6d0d100c0d --- /dev/null +++ b/mod/assign/submission/onlinetext/templates/email_summary_html.mustache @@ -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 . +}} +{{! + @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 + } +}} +

{{#str}}onlinetext, assignsubmission_onlinetext{{/str}}

+

{{#str}}numwords, assignsubmission_onlinetext, {{wordcount}}{{/str}}

diff --git a/mod/assign/submission/onlinetext/templates/email_summary_text.mustache b/mod/assign/submission/onlinetext/templates/email_summary_text.mustache new file mode 100644 index 00000000000..39a0c59e6b7 --- /dev/null +++ b/mod/assign/submission/onlinetext/templates/email_summary_text.mustache @@ -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 . +}} +{{! + @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}} diff --git a/mod/assign/submissionplugin.php b/mod/assign/submissionplugin.php index 2fad8a9fcb3..226df3a8c50 100644 --- a/mod/assign/submissionplugin.php +++ b/mod/assign/submissionplugin.php @@ -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

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 ['', '']; + } } diff --git a/mod/assign/tests/notification_helper_test.php b/mod/assign/tests/notification_helper_test.php index 3e8a9931a03..8675e396084 100644 --- a/mod/assign/tests/notification_helper_test.php +++ b/mod/assign/tests/notification_helper_test.php @@ -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 = [ + '

Your assignment for Assignment 1 has been successfully submitted.

', + '

You can view your submission and check its status on the assignment page.

', + '

Your submission contains:

', + '

Online text

', + '

(3 words)

', + '

File submissions

', + '
  • submissionsample01.txt (42 bytes)
  • ', + '
  • submissionsample02.txt (42 bytes)
  • ', + ]; + foreach ($expectedfragments as $html) { + $this->assertStringContainsString($html, $message->fullmessagehtml); + } + + // Clear sink. + $sink->clear(); + } }