-
-
Notifications
You must be signed in to change notification settings - Fork 177
feat: Add automatic assignment deadline reminder system #1044
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,193 @@ | ||||||||||||
| """ | ||||||||||||
| Assignment deadline reminder system for Alpha One Labs. | ||||||||||||
|
|
||||||||||||
| Handles automatic sending of reminder emails to students when assignments | ||||||||||||
| are due soon. Uses Django signals to automatically create and manage reminders. | ||||||||||||
| """ | ||||||||||||
|
|
||||||||||||
| import logging | ||||||||||||
| from datetime import timedelta | ||||||||||||
|
|
||||||||||||
| from django.conf import settings | ||||||||||||
| from django.core.mail import send_mail | ||||||||||||
| from django.db.models.signals import post_save | ||||||||||||
| from django.dispatch import receiver | ||||||||||||
| from django.template.loader import render_to_string | ||||||||||||
| from django.utils import timezone | ||||||||||||
|
|
||||||||||||
| from .models import CourseMaterial, Enrollment | ||||||||||||
|
|
||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class ReminderScheduler: | ||||||||||||
| """Manages scheduling and sending of assignment deadline reminders.""" | ||||||||||||
|
|
||||||||||||
| @staticmethod | ||||||||||||
| def should_send_early_reminder(course_material): | ||||||||||||
| """ | ||||||||||||
| Check if early reminder (24 hours before) should be sent. | ||||||||||||
|
|
||||||||||||
| Returns True if: | ||||||||||||
| - Material has a due_date | ||||||||||||
| - Reminder hasn't been sent yet | ||||||||||||
| - Deadline is within 24-48 hours from now | ||||||||||||
| """ | ||||||||||||
| if not course_material.due_date or course_material.reminder_sent: | ||||||||||||
| return False | ||||||||||||
|
|
||||||||||||
| now = timezone.now() | ||||||||||||
| time_until_due = course_material.due_date - now | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| return timedelta(hours=24) <= time_until_due <= timedelta(hours=48) | ||||||||||||
|
|
||||||||||||
| @staticmethod | ||||||||||||
| def should_send_final_reminder(course_material): | ||||||||||||
| """ | ||||||||||||
| Check if final reminder (on deadline day) should be sent. | ||||||||||||
|
|
||||||||||||
| Returns True if: | ||||||||||||
| - Material has a due_date | ||||||||||||
| - Final reminder hasn't been sent yet | ||||||||||||
| - Deadline is within next 24 hours | ||||||||||||
| """ | ||||||||||||
| if not course_material.due_date or course_material.final_reminder_sent: | ||||||||||||
| return False | ||||||||||||
|
|
||||||||||||
| now = timezone.now() | ||||||||||||
| time_until_due = course_material.due_date - now | ||||||||||||
|
|
||||||||||||
| # Send final reminder if deadline is within 24 hours | ||||||||||||
| return timedelta(0) <= time_until_due <= timedelta(hours=24) | ||||||||||||
|
|
||||||||||||
| @staticmethod | ||||||||||||
| def send_early_reminder(course_material): | ||||||||||||
| """Send 24-hour before deadline reminder to all enrolled students.""" | ||||||||||||
| try: | ||||||||||||
| course = course_material.course | ||||||||||||
| enrollments = Enrollment.objects.filter( | ||||||||||||
| course=course, | ||||||||||||
| status="approved" | ||||||||||||
| ).select_related("student", "student__profile") | ||||||||||||
|
|
||||||||||||
| for enrollment in enrollments: | ||||||||||||
| student = enrollment.student | ||||||||||||
| context = { | ||||||||||||
| "student_name": student.first_name or student.username, | ||||||||||||
| "assignment_title": course_material.title, | ||||||||||||
| "course_title": course.title, | ||||||||||||
| "due_date": course_material.due_date, | ||||||||||||
| "description": course_material.description, | ||||||||||||
| "hours_remaining": 24, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| subject = f"Reminder: '{course_material.title}' is due in 24 hours" | ||||||||||||
| html_message = render_to_string( | ||||||||||||
| "emails/assignment_reminder_24h.html", | ||||||||||||
| context | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| send_mail( | ||||||||||||
| subject=subject, | ||||||||||||
| message=strip_tags(html_message), | ||||||||||||
| from_email=settings.DEFAULT_FROM_EMAIL, | ||||||||||||
| recipient_list=[student.email], | ||||||||||||
| html_message=html_message, | ||||||||||||
| fail_silently=False, | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| logger.info( | ||||||||||||
| f"24-hour reminder sent to {student.username} for " | ||||||||||||
| f"assignment '{course_material.title}'" | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| # Mark reminder as sent | ||||||||||||
| course_material.reminder_sent = True | ||||||||||||
| course_material.save(update_fields=["reminder_sent"]) | ||||||||||||
| logger.info(f"Early reminder sent for {course_material.title}") | ||||||||||||
|
|
||||||||||||
| except Exception as e: | ||||||||||||
| logger.error( | ||||||||||||
| f"Error sending early reminder for {course_material.title}: {str(e)}" | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
|
Comment on lines
+67
to
+114
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A single email failure currently aborts the entire recipient batch. The π§ Suggested structure- try:
- ...
- for enrollment in enrollments:
- ...
- send_mail(...)
- course_material.reminder_sent = True
- course_material.save(update_fields=["reminder_sent"])
- except Exception as e:
- logger.error(...)
+ sent_any = False
+ for enrollment in enrollments:
+ try:
+ ...
+ send_mail(...)
+ sent_any = True
+ except Exception:
+ logger.exception(
+ "Failed sending reminder for material_id=%s to user_id=%s",
+ course_material.id,
+ enrollment.student_id,
+ )
+ continue
+
+ if sent_any:
+ course_material.reminder_sent = True
+ course_material.save(update_fields=["reminder_sent"])Also applies to: 118-164 π§° Toolsπͺ Ruff (0.15.6)[warning] 101-102: Logging statement uses f-string (G004) [warning] 108-108: Logging statement uses f-string (G004) [warning] 110-110: Do not catch blind exception: (BLE001) [warning] 111-113: Use Replace with (TRY400) [warning] 112-112: Logging statement uses f-string (G004) [warning] 112-112: Use explicit conversion flag Replace with conversion flag (RUF010) π€ Prompt for AI Agents |
||||||||||||
| @staticmethod | ||||||||||||
| def send_final_reminder(course_material): | ||||||||||||
| """Send final reminder on deadline day to all enrolled students.""" | ||||||||||||
| try: | ||||||||||||
| course = course_material.course | ||||||||||||
| enrollments = Enrollment.objects.filter( | ||||||||||||
| course=course, | ||||||||||||
| status="approved" | ||||||||||||
| ).select_related("student", "student__profile") | ||||||||||||
|
|
||||||||||||
| for enrollment in enrollments: | ||||||||||||
| student = enrollment.student | ||||||||||||
| context = { | ||||||||||||
| "student_name": student.first_name or student.username, | ||||||||||||
| "assignment_title": course_material.title, | ||||||||||||
| "course_title": course.title, | ||||||||||||
| "due_date": course_material.due_date, | ||||||||||||
| "description": course_material.description, | ||||||||||||
| "is_final": True, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| subject = f"FINAL REMINDER: '{course_material.title}' is due today!" | ||||||||||||
| html_message = render_to_string( | ||||||||||||
| "emails/assignment_reminder_final.html", | ||||||||||||
| context | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| send_mail( | ||||||||||||
| subject=subject, | ||||||||||||
| message=strip_tags(html_message), | ||||||||||||
| from_email=settings.DEFAULT_FROM_EMAIL, | ||||||||||||
| recipient_list=[student.email], | ||||||||||||
| html_message=html_message, | ||||||||||||
| fail_silently=False, | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| logger.info( | ||||||||||||
| f"Final reminder sent to {student.username} for " | ||||||||||||
| f"assignment '{course_material.title}'" | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| # Mark final reminder as sent | ||||||||||||
| course_material.final_reminder_sent = True | ||||||||||||
| course_material.save(update_fields=["final_reminder_sent"]) | ||||||||||||
| logger.info(f"Final reminder sent for {course_material.title}") | ||||||||||||
|
|
||||||||||||
| except Exception as e: | ||||||||||||
| logger.error( | ||||||||||||
| f"Error sending final reminder for {course_material.title}: {str(e)}" | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @receiver(post_save, sender=CourseMaterial) | ||||||||||||
| def check_and_send_assignment_reminders(sender, instance, created, **kwargs): | ||||||||||||
|
Comment on lines
+167
to
+168
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§© Analysis chainπ Script executed: find . -type f -name "assignment_reminders.py" | head -5Repository: alphaonelabs/website Length of output: 93 π Script executed: git ls-files | grep -E "(assignment_reminder|signal)" | head -20Repository: alphaonelabs/website Length of output: 390 π Script executed: fd "assignment_reminder" --type fRepository: alphaonelabs/website Length of output: 375 π Script executed: wc -l ./web/assignment_reminders.pyRepository: alphaonelabs/website Length of output: 97 π Script executed: cat -n ./web/assignment_reminders.py | sed -n '160,175p'Repository: alphaonelabs/website Length of output: 768 π Script executed: cat -n ./web/assignment_reminders.py | head -50Repository: alphaonelabs/website Length of output: 1893 π Script executed: cat -n ./web/assignment_reminders.py | sed -n '165,193p'Repository: alphaonelabs/website Length of output: 1326 π Script executed: grep -r "assignment_reminders" --include="*.py" | grep -E "(import|from)" | head -20Repository: alphaonelabs/website Length of output: 459 π Script executed: cat ./web/signals.py 2>/dev/null || echo "File not found"Repository: alphaonelabs/website Length of output: 3000 π Script executed: cat ./web/apps.py 2>/dev/null | head -50Repository: alphaonelabs/website Length of output: 309 Add This receiver is imported via side effects in Minimal fix-@receiver(post_save, sender=CourseMaterial)
+@receiver(
+ post_save,
+ sender=CourseMaterial,
+ dispatch_uid="web.assignment_reminders.check_and_send_assignment_reminders",
+)
def check_and_send_assignment_reminders(sender, instance, created, **kwargs):π§° Toolsπͺ Ruff (0.15.6)[warning] 168-168: Unused function argument: (ARG001) [warning] 168-168: Unused function argument: (ARG001) [warning] 168-168: Missing type annotation for (ANN003) [warning] 168-168: Unused function argument: (ARG001) π€ Prompt for AI Agents |
||||||||||||
| """ | ||||||||||||
| Signal handler to check and send assignment reminders when CourseMaterial is saved. | ||||||||||||
|
|
||||||||||||
| This runs after every save of a CourseMaterial object and: | ||||||||||||
| 1. Sends 24-hour early reminder if deadline is 24-48 hours away | ||||||||||||
| 2. Sends final reminder if deadline is within 24 hours | ||||||||||||
| """ | ||||||||||||
| # Only process if material is an assignment with a due date | ||||||||||||
| if instance.material_type != "assignment" or not instance.due_date: | ||||||||||||
| return | ||||||||||||
|
|
||||||||||||
| # Check and send early reminder | ||||||||||||
| if ReminderScheduler.should_send_early_reminder(instance): | ||||||||||||
| ReminderScheduler.send_early_reminder(instance) | ||||||||||||
|
|
||||||||||||
| # Check and send final reminder | ||||||||||||
| if ReminderScheduler.should_send_final_reminder(instance): | ||||||||||||
| ReminderScheduler.send_final_reminder(instance) | ||||||||||||
|
Comment on lines
+167
to
+186
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§© Analysis chainπ Script executed: # First, find the ReminderScheduler class definition
rg "class ReminderScheduler" -A 10Repository: alphaonelabs/website Length of output: 1493 π Script executed: # Search for send_early_reminder and send_final_reminder implementations
rg "def send_early_reminder|def send_final_reminder" -A 5Repository: alphaonelabs/website Length of output: 930 π Script executed: # Check for async task definitions and queue setup
rg "celery|@task|@shared_task|background_task|defer" --type py | head -20Repository: alphaonelabs/website Length of output: 46 π Script executed: # Find imports in assignment_reminders.py to understand dependencies
fd "assignment_reminders.py" | xargs head -30Repository: alphaonelabs/website Length of output: 3758 π Script executed: # Get the complete implementation of send_early_reminder and send_final_reminder
fd "assignment_reminders.py" | xargs cat -n | sed -n '1,200p'Repository: alphaonelabs/website Length of output: 8748 π Script executed: # Check the web/notifications.py file to see the deferred approach
fd "notifications.py" -path "*/web/*" | xargs cat -n | head -100Repository: alphaonelabs/website Length of output: 232 π Script executed: # Search for notifications.py
find . -name "notifications.py" -path "*/web/*"Repository: alphaonelabs/website Length of output: 86 π Script executed: # Get the send_assignment_reminders function from notifications.py
rg "def send_assignment_reminders" -A 30Repository: alphaonelabs/website Length of output: 2280 π Script executed: # See full context of send_assignment_reminders in notifications.py
cat web/notifications.py | head -80Repository: alphaonelabs/website Length of output: 2582 π Script executed: # Check if Celery or any task queue is configured
rg "celery|CELERY|@task|@shared_task" --type py | head -20Repository: alphaonelabs/website Length of output: 46 π Script executed: # Check CourseMaterial model for the reminder_sent and final_reminder_sent fields
rg "class CourseMaterial" -A 30Repository: alphaonelabs/website Length of output: 7303 Move email sending outside the post_save signal using Sending emails synchronously in a For this pattern, either queue the reminder sending using Celery/Huey, or use Django's π§° Toolsπͺ Ruff (0.15.6)[warning] 168-168: Unused function argument: (ARG001) [warning] 168-168: Unused function argument: (ARG001) [warning] 168-168: Missing type annotation for (ANN003) [warning] 168-168: Unused function argument: (ARG001) π€ Prompt for AI Agents
Comment on lines
+181
to
+186
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reminder deduplication is non-atomic under concurrency.
|
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def strip_tags(html): | ||||||||||||
| """Utility function to strip HTML tags for plain text emails.""" | ||||||||||||
| import re | ||||||||||||
| clean = re.compile('<.*?>') | ||||||||||||
| return re.sub(clean, '', html) | ||||||||||||
|
Comment on lines
+189
to
+193
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§Ή Nitpick | π΅ Trivial Use Djangoβs built-in The regex approach is brittle for real HTML and is less maintainable than β Minimal fix+from django.utils.html import strip_tags
...
-def strip_tags(html):
- """Utility function to strip HTML tags for plain text emails."""
- import re
- clean = re.compile('<.*?>')
- return re.sub(clean, '', html)π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| <!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
|
Comment on lines
+2
to
+3
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add required document metadata for accessibility.
β Minimal fix-<html>
+<html lang="en">
<head>
+ <title>24-Hour Assignment Reminder</title>As per coding guidelines "Ensure proper HTML structure and accessibility in templates". Also applies to: 56-56 π§° Toolsπͺ HTMLHint (1.9.2)[warning] 2-2: An lang attribute must be present on elements. (html-lang-require) π€ Prompt for AI Agents |
||
| <style> | ||
| body { | ||
| font-family: Arial, sans-serif; | ||
| line-height: 1.6; | ||
| color: #333; | ||
| max-width: 600px; | ||
| margin: 0 auto; | ||
| padding: 20px; | ||
| } | ||
| .container { | ||
| background-color: #f8f9fa; | ||
| border-radius: 8px; | ||
| padding: 30px; | ||
| border-left: 4px solid #FFA500; | ||
| } | ||
| .header { | ||
| color: #FFA500; | ||
| font-size: 24px; | ||
| font-weight: bold; | ||
| margin-bottom: 20px; | ||
| } | ||
| .course-name { | ||
| background-color: #fff3cd; | ||
| border-left: 4px solid #FFA500; | ||
| padding: 15px; | ||
| margin: 20px 0; | ||
| border-radius: 4px; | ||
| } | ||
| .due-date { | ||
| font-size: 18px; | ||
| color: #d9534f; | ||
| font-weight: bold; | ||
| margin: 10px 0; | ||
| } | ||
| .footer { | ||
| margin-top: 30px; | ||
| padding-top: 20px; | ||
| border-top: 1px solid #ddd; | ||
| font-size: 12px; | ||
| color: #666; | ||
| } | ||
| .button { | ||
| display: inline-block; | ||
| background-color: #FFA500; | ||
| color: white; | ||
| padding: 12px 24px; | ||
| text-decoration: none; | ||
| border-radius: 4px; | ||
| margin: 20px 0; | ||
| font-weight: bold; | ||
| } | ||
| </style> | ||
|
Comment on lines
+4
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Template styling does not follow project HTML/Tailwind standards. The template relies on inline CSS, custom classes, and hex color values rather than required Tailwind utilities, project color tokens, and dark-mode variants. As per coding guidelines "Always use Tailwind CSS classes for styling HTML elements", "Never use inline styles", and "Always include dark mode alternatives using dark: prefix". π€ Prompt for AI Agents |
||
| </head> | ||
| <body> | ||
| <div class="container"> | ||
| <div class="header">β° Assignment Due in 24 Hours</div> | ||
|
|
||
| <p>Hi {{ student_name }},</p> | ||
|
|
||
| <p>This is a reminder that you have an assignment due soon in <strong>{{ course_title }}</strong>.</p> | ||
|
|
||
| <div class="course-name"> | ||
| <strong>Assignment:</strong> {{ assignment_title }}<br> | ||
| <div class="due-date">Due: {{ due_date|date:"F j, Y \a\t g:i A" }}</div> | ||
| </div> | ||
|
|
||
| {% if description %} | ||
| <p><strong>Details:</strong></p> | ||
| <p>{{ description }}</p> | ||
| {% endif %} | ||
|
|
||
| <p>Make sure to submit your work before the deadline to avoid any late submissions.</p> | ||
|
|
||
| <p>Need help? Visit the course materials or reach out to your instructor.</p> | ||
|
|
||
| <p> | ||
| <a href="{{ site_url }}" class="button">View Assignment</a> | ||
| </p> | ||
|
|
||
| <div class="footer"> | ||
| <p>Alpha One Labs Education Platform<br> | ||
| This is an automated reminder. Please do not reply to this email.</p> | ||
| </div> | ||
| </div> | ||
| </body> | ||
| </html> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π οΈ Refactor suggestion | π Major
Add function/method type hints for this module.
Multiple methods and functions still miss parameter/return annotations; adding them will improve linting and maintainability.
As per coding guidelines "
**/*.py: Use type hints in Python where appropriate".Also applies to: 46-46, 65-65, 116-116, 168-168, 189-189
π§° Tools
πͺ Ruff (0.15.6)
[warning] 27-27: Missing return type annotation for staticmethod
should_send_early_reminder(ANN205)
π€ Prompt for AI Agents