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}}

+ + 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(); + } }