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 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{{#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, sees 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.
', + '(3 words)
', + '