Skip to content

Commit 3f5e84e

Browse files
Improve score notification context
1 parent 63c463b commit 3f5e84e

4 files changed

Lines changed: 210 additions & 5 deletions

File tree

accounts/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class Meta:
4141
"country": "Used to prefill course registration forms.",
4242
"registration_role": "Used to prefill course registration forms.",
4343
"email_submission_confirmations": (
44-
"Sends you a copy of the submitted results after you submit "
44+
"Sends confirmation and score emails after you submit "
4545
"homework or a project."
4646
),
4747
"email_deadline_reminders": (

accounts/tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ def test_account_settings_shows_email_preference_categories(self):
244244
)
245245
self.assertContains(
246246
response,
247-
"Sends you a copy of the submitted results",
247+
"Sends confirmation and score emails",
248248
)
249249
self.assertContains(response, "Deadline reminders")
250250
self.assertContains(response, "within 24 hours")

course_management/datamailer.py

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,15 @@ def homework_submission_recipient_list_payload(
415415
"learning_in_public_score": submission.learning_in_public_score,
416416
"faq_score": submission.faq_score,
417417
"total_score": submission.total_score,
418+
"homework_url": public_url(
419+
reverse(
420+
"homework",
421+
kwargs={
422+
"course_slug": course.slug,
423+
"homework_slug": homework.slug,
424+
},
425+
)
426+
),
418427
}
419428
payload = recipient_list_member_payload(
420429
list_type="homework_submitters",
@@ -457,6 +466,18 @@ def project_submission_recipient_list_payload(
457466
submission.peer_review_learning_in_public_score
458467
),
459468
"total_score": submission.total_score,
469+
"github_link": submission.github_link,
470+
"commit_id": submission.commit_id,
471+
"faq_contribution_url": submission.faq_contribution_url or "",
472+
"project_url": public_url(
473+
reverse(
474+
"project",
475+
kwargs={
476+
"course_slug": course.slug,
477+
"project_slug": project.slug,
478+
},
479+
)
480+
),
460481
"reviewed_enough_peers": submission.reviewed_enough_peers,
461482
"passed": submission.passed,
462483
}
@@ -621,6 +642,8 @@ def homework_score_notification_members(
621642
).order_by("student_id", "-submitted_at", "-id")
622643
seen_students = set()
623644
for submission in submissions:
645+
if not getattr(submission.student, "email_submission_confirmations", True):
646+
continue
624647
if submission.student_id in seen_students:
625648
continue
626649
item = homework_submission_recipient_list_payload(submission)
@@ -653,6 +676,8 @@ def project_score_notification_members(
653676
"student", "project__course"
654677
).order_by("id")
655678
for submission in submissions:
679+
if not getattr(submission.student, "email_submission_confirmations", True):
680+
continue
656681
item = project_submission_recipient_list_payload(submission)
657682
if item is None:
658683
continue
@@ -680,6 +705,19 @@ def homework_score_notification_payload(
680705
course_url = public_url(
681706
reverse("course", kwargs={"course_slug": course.slug})
682707
)
708+
homework_url = public_url(
709+
reverse(
710+
"homework",
711+
kwargs={
712+
"course_slug": course.slug,
713+
"homework_slug": homework.slug,
714+
},
715+
)
716+
)
717+
leaderboard_url = public_url(
718+
reverse("leaderboard", kwargs={"course_slug": course.slug})
719+
)
720+
profile_url = public_url(reverse("account_settings"))
683721

684722
payload = {
685723
"audience": config.audience,
@@ -692,7 +730,20 @@ def homework_score_notification_payload(
692730
"homework_slug": homework.slug,
693731
"homework_title": homework.title,
694732
"course_url": course_url,
695-
"scores_url": course_url,
733+
"homework_url": homework_url,
734+
"scores_url": homework_url,
735+
"leaderboard_url": leaderboard_url,
736+
"profile_url": profile_url,
737+
"notification_footer": (
738+
f"You are receiving this because you submitted {homework.title} "
739+
f"for {course.title} and homework/project submission emails "
740+
"are enabled in your profile."
741+
),
742+
"notification_footer_text": (
743+
"If you don't want to receive homework/project submission "
744+
"and score emails, turn off homework and project submission "
745+
f"emails in your profile: {profile_url}"
746+
),
696747
},
697748
"list": list_data,
698749
"members": members,
@@ -704,6 +755,8 @@ def homework_score_notification_payload(
704755
"course_slug": course.slug,
705756
"homework_slug": homework.slug,
706757
"homework_id": homework.pk,
758+
"preference_key": "email_submission_confirmations",
759+
"cmp_preference_key": "email_submission_confirmations",
707760
},
708761
}
709762
if config.from_email:
@@ -731,6 +784,22 @@ def project_score_notification_payload(
731784
},
732785
)
733786
)
787+
project_results_url = public_url(
788+
reverse(
789+
"project_results",
790+
kwargs={
791+
"course_slug": course.slug,
792+
"project_slug": project.slug,
793+
},
794+
)
795+
)
796+
course_url = public_url(
797+
reverse("course", kwargs={"course_slug": course.slug})
798+
)
799+
leaderboard_url = public_url(
800+
reverse("leaderboard", kwargs={"course_slug": course.slug})
801+
)
802+
profile_url = public_url(reverse("account_settings"))
734803

735804
payload = {
736805
"audience": config.audience,
@@ -742,8 +811,22 @@ def project_score_notification_payload(
742811
"course_title": course.title,
743812
"project_slug": project.slug,
744813
"project_title": project.title,
814+
"course_url": course_url,
745815
"project_url": project_url,
746-
"scores_url": project_url,
816+
"project_results_url": project_results_url,
817+
"scores_url": project_results_url,
818+
"leaderboard_url": leaderboard_url,
819+
"profile_url": profile_url,
820+
"notification_footer": (
821+
f"You are receiving this because you submitted {project.title} "
822+
f"for {course.title} and homework/project submission emails "
823+
"are enabled in your profile."
824+
),
825+
"notification_footer_text": (
826+
"If you don't want to receive homework/project submission "
827+
"and score emails, turn off homework and project submission "
828+
f"emails in your profile: {profile_url}"
829+
),
747830
},
748831
"list": list_data,
749832
"members": members,
@@ -755,6 +838,8 @@ def project_score_notification_payload(
755838
"course_slug": course.slug,
756839
"project_slug": project.slug,
757840
"project_id": project.pk,
841+
"preference_key": "email_submission_confirmations",
842+
"cmp_preference_key": "email_submission_confirmations",
758843
},
759844
}
760845
if config.from_email:

courses/tests/test_datamailer.py

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,23 @@ def test_homework_score_notification_payload_targets_homework_submitters(
783783
self.assertEqual(payload["from_email"], "courses")
784784
self.assertEqual(
785785
payload["context"]["scores_url"],
786-
"https://courses.example.com/ml-zoomcamp-2026/",
786+
"https://courses.example.com/ml-zoomcamp-2026/homework/homework-1",
787+
)
788+
self.assertEqual(
789+
payload["context"]["leaderboard_url"],
790+
"https://courses.example.com/ml-zoomcamp-2026/leaderboard",
791+
)
792+
self.assertEqual(
793+
payload["context"]["profile_url"],
794+
"https://courses.example.com/accounts/settings/",
795+
)
796+
self.assertIn(
797+
"you submitted Homework 1",
798+
payload["context"]["notification_footer"],
799+
)
800+
self.assertEqual(
801+
payload["metadata"]["preference_key"],
802+
"email_submission_confirmations",
787803
)
788804
self.assertEqual(payload["member_sync"], "reconcile")
789805
self.assertTrue(payload["remove_absent_members"])
@@ -804,6 +820,10 @@ def test_homework_score_notification_payload_targets_homework_submitters(
804820
)
805821
self.assertEqual(member["metadata"]["faq_score"], 1)
806822
self.assertEqual(member["metadata"]["total_score"], 9)
823+
self.assertEqual(
824+
member["metadata"]["homework_url"],
825+
"https://courses.example.com/ml-zoomcamp-2026/homework/homework-1",
826+
)
807827

808828
@override_settings(**DATAMAILER_SETTINGS)
809829
def test_homework_score_notification_payload_dedupes_student_submissions(
@@ -857,6 +877,40 @@ def test_homework_score_notification_payload_dedupes_student_submissions(
857877
self.assertEqual(member["email"], "learner@example.com")
858878
self.assertEqual(member["metadata"]["total_score"], 9)
859879

880+
@override_settings(**DATAMAILER_SETTINGS)
881+
def test_homework_score_notification_skips_opted_out_students(self):
882+
course = Course.objects.create(
883+
slug="ml-zoomcamp-2026",
884+
title="ML Zoomcamp 2026",
885+
description="Machine learning",
886+
)
887+
homework = Homework.objects.create(
888+
course=course,
889+
slug="homework-1",
890+
title="Homework 1",
891+
due_date="2026-01-01T00:00:00Z",
892+
)
893+
user = CustomUser.objects.create_user(
894+
username="learner@example.com",
895+
email="learner@example.com",
896+
password="test",
897+
email_submission_confirmations=False,
898+
)
899+
enrollment = Enrollment.objects.create(
900+
student=user,
901+
course=course,
902+
)
903+
Submission.objects.create(
904+
homework=homework,
905+
student=user,
906+
enrollment=enrollment,
907+
total_score=9,
908+
)
909+
910+
_, payload = homework_score_notification_payload(homework)
911+
912+
self.assertEqual(payload["members"], [])
913+
860914
@override_settings(**DATAMAILER_SETTINGS)
861915
@patch(
862916
"course_management.datamailer.DatamailerClient.send_recipient_list_transactional"
@@ -946,8 +1000,28 @@ def test_project_score_notification_payload_targets_project_submitters(
9461000
self.assertEqual(payload["from_email"], "courses")
9471001
self.assertEqual(
9481002
payload["context"]["scores_url"],
1003+
"https://courses.example.com/ml-zoomcamp-2026/project/project-1/results",
1004+
)
1005+
self.assertEqual(
1006+
payload["context"]["project_url"],
9491007
"https://courses.example.com/ml-zoomcamp-2026/project/project-1",
9501008
)
1009+
self.assertEqual(
1010+
payload["context"]["leaderboard_url"],
1011+
"https://courses.example.com/ml-zoomcamp-2026/leaderboard",
1012+
)
1013+
self.assertEqual(
1014+
payload["context"]["profile_url"],
1015+
"https://courses.example.com/accounts/settings/",
1016+
)
1017+
self.assertIn(
1018+
"you submitted Project 1",
1019+
payload["context"]["notification_footer"],
1020+
)
1021+
self.assertEqual(
1022+
payload["metadata"]["preference_key"],
1023+
"email_submission_confirmations",
1024+
)
9511025
self.assertEqual(payload["member_sync"], "reconcile")
9521026
self.assertTrue(payload["remove_absent_members"])
9531027
self.assertEqual(payload["list"]["type"], "project_submitters")
@@ -970,9 +1044,55 @@ def test_project_score_notification_payload_targets_project_submitters(
9701044
4,
9711045
)
9721046
self.assertEqual(member["metadata"]["total_score"], 98)
1047+
self.assertEqual(
1048+
member["metadata"]["github_link"],
1049+
"https://github.com/example/project",
1050+
)
1051+
self.assertEqual(member["metadata"]["commit_id"], "abc123")
1052+
self.assertEqual(
1053+
member["metadata"]["project_url"],
1054+
"https://courses.example.com/ml-zoomcamp-2026/project/project-1",
1055+
)
9731056
self.assertTrue(member["metadata"]["reviewed_enough_peers"])
9741057
self.assertTrue(member["metadata"]["passed"])
9751058

1059+
@override_settings(**DATAMAILER_SETTINGS)
1060+
def test_project_score_notification_skips_opted_out_students(self):
1061+
course = Course.objects.create(
1062+
slug="ml-zoomcamp-2026",
1063+
title="ML Zoomcamp 2026",
1064+
description="Machine learning",
1065+
)
1066+
project = Project.objects.create(
1067+
course=course,
1068+
slug="project-1",
1069+
title="Project 1",
1070+
submission_due_date="2026-01-01T00:00:00Z",
1071+
peer_review_due_date="2026-01-08T00:00:00Z",
1072+
)
1073+
user = CustomUser.objects.create_user(
1074+
username="project-learner@example.com",
1075+
email="project-learner@example.com",
1076+
password="test",
1077+
email_submission_confirmations=False,
1078+
)
1079+
enrollment = Enrollment.objects.create(
1080+
student=user,
1081+
course=course,
1082+
)
1083+
ProjectSubmission.objects.create(
1084+
project=project,
1085+
student=user,
1086+
enrollment=enrollment,
1087+
github_link="https://github.com/example/project",
1088+
commit_id="abc123",
1089+
total_score=98,
1090+
)
1091+
1092+
_, payload = project_score_notification_payload(project)
1093+
1094+
self.assertEqual(payload["members"], [])
1095+
9761096
@override_settings(**DATAMAILER_SETTINGS)
9771097
@patch(
9781098
"course_management.datamailer.DatamailerClient.send_recipient_list_transactional"

0 commit comments

Comments
 (0)