From ce2d230d843af5face90c9d87f02b32b04202174 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:07:31 +0000 Subject: [PATCH 1/4] Initial plan From 8e01e7f0f911d24f47d4382a0ed5afd3fc20152a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:14:55 +0000 Subject: [PATCH 2/4] Add personalized reminder emails with organization and time info Co-authored-by: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> --- .../commands/cron_send_reminders.py | 112 +++++++++++------- .../templates/website/reminder_settings.html | 17 ++- website/views/daily_reminders.py | 92 ++++++++++++-- 3 files changed, 171 insertions(+), 50 deletions(-) diff --git a/website/management/commands/cron_send_reminders.py b/website/management/commands/cron_send_reminders.py index 665ced381c..726834499f 100644 --- a/website/management/commands/cron_send_reminders.py +++ b/website/management/commands/cron_send_reminders.py @@ -3,7 +3,6 @@ import random import time from datetime import time as dt_time -from itertools import islice from django.conf import settings from django.core.mail import EmailMultiAlternatives @@ -15,12 +14,6 @@ logger = logging.getLogger(__name__) -def batch(iterable, size): - """Helper function to create batches from an iterable""" - iterator = iter(iterable) - return iter(lambda: list(islice(iterator, size)), []) - - class Command(LoggedBaseCommand): help = "Sends daily check-in reminders to users who haven't checked in today" @@ -76,7 +69,7 @@ def handle(self, *args, **options): user_ids = [rs.user_id for rs in active_settings] profile_map = {profile.user_id: profile for profile in UserProfile.objects.filter(user_id__in=user_ids)} - users_needing_reminders = [] + reminders_to_send = [] for reminder_settings in active_settings: try: @@ -85,7 +78,7 @@ def handle(self, *args, **options): if profile and profile.last_check_in and profile.last_check_in == now.date(): continue - users_needing_reminders.append(reminder_settings.user) + reminders_to_send.append((reminder_settings, profile)) logger.info( f"User {reminder_settings.user.username} added to reminder list for time {reminder_settings.reminder_time} ({reminder_settings.timezone})" ) @@ -94,33 +87,63 @@ def handle(self, *args, **options): logger.error(f"Error processing user {reminder_settings.user.username}: {str(e)}") continue - if not users_needing_reminders: + if not reminders_to_send: logger.info("No users need reminders at this time") return - # Process users in batches of 50 - batch_size = 50 - successful_batches = 0 - failed_batches = 0 - total_users = len(users_needing_reminders) + # Process reminders individually for personalization + successful_count = 0 + failed_count = 0 + total_users = len(reminders_to_send) - for i, user_batch in enumerate(batch(users_needing_reminders, batch_size), 1): + for i, (reminder_settings, profile) in enumerate(reminders_to_send, 1): try: - # Add random delay between batches (1-5 seconds) if not the first batch - if i > 1: - delay = random.uniform(1, 5) - logger.info(f"Waiting {delay:.2f} seconds before processing batch {i}") + # Add small delay between emails to avoid overwhelming the server + if i > 1 and i % 10 == 0: + delay = random.uniform(0.5, 2) + logger.info(f"Processed {i} emails, waiting {delay:.2f} seconds") time.sleep(delay) + user = reminder_settings.user + + # Get organization info + org_name = "" + org_info_html = "" + if profile and profile.team: + org_name = profile.team.name + org_info_html = f""" +
+

Organization: {org_name}

+
+ """ + + # Format reminder time in user's timezone + reminder_time_str = reminder_settings.reminder_time.strftime("%I:%M %p") + timezone_str = reminder_settings.timezone + # Create email message + plain_body = f"""Hello {user.username}, + +This is your daily check-in reminder for {org_name if org_name else 'your team'}. + +Reminder Time: {reminder_time_str} ({timezone_str}) + +Click here to check in: https://{settings.PRODUCTION_DOMAIN}/add-sizzle-checkin/ + +You can manage your reminder settings at: https://{settings.PRODUCTION_DOMAIN}/reminder-settings/ + +Regular check-ins help keep your team informed about your progress and any challenges you might be facing. + +Thank you for keeping your team updated! + +Best regards, +The BLT Team""" + email = EmailMultiAlternatives( subject="Daily Check-in Reminder", - body="It's time for your daily check-in! Please log in to update your status.\n\nClick here to check in: https://" - + settings.PRODUCTION_DOMAIN - + "/add-sizzle-checkin/", + body=plain_body, from_email=settings.DEFAULT_FROM_EMAIL, - to=[settings.DEFAULT_FROM_EMAIL], # Send to a single recipient - bcc=[user.email for user in user_batch], # BCC all users in batch + to=[user.email], ) # Add HTML content @@ -129,15 +152,26 @@ def handle(self, *args, **options):

Daily Check-in Reminder

-

It's time for your daily check-in! Please log in to update your status.

-
+

Hello {user.username},

+

It's time for your daily check-in{f" for {org_name}" if org_name else ""}! Please log in to update your status.

+ {org_info_html} +
+

Your Reminder Time: {reminder_time_str} ({timezone_str})

+
+
Check In Now

Regular check-ins help keep your team informed about your progress and any challenges you might be facing.

-

Thank you for keeping your team updated!

+
+

+ Manage your reminder settings +

+
+

Thank you for keeping your team updated!

+

Best regards,
The BLT Team

@@ -147,30 +181,28 @@ def handle(self, *args, **options): # Send email email.send() - # Update last_reminder_sent for successful batch - ReminderSettings.objects.filter(user_id__in=[user.id for user in user_batch]).update( - last_reminder_sent=now - ) + # Update last_reminder_sent + reminder_settings.last_reminder_sent = now + reminder_settings.save(update_fields=["last_reminder_sent"]) - successful_batches += 1 - logger.info(f"Successfully sent batch {i} to {len(user_batch)} users") + successful_count += 1 + logger.info(f"Successfully sent reminder to {user.username} ({user.email})") except Exception as e: - failed_batches += 1 - logger.error(f"Error sending batch {i}: {str(e)}") + failed_count += 1 + logger.error(f"Error sending reminder to {reminder_settings.user.username}: {str(e)}") # Log summary logger.info( f""" Reminder Summary: - Total users processed: {total_users} - - Successful batches: {successful_batches} - - Failed batches: {failed_batches} - - Batch size: {batch_size} + - Successfully sent: {successful_count} + - Failed: {failed_count} """ ) - return f"Processed {total_users} users, {successful_batches} successful batches, {failed_batches} failed batches" + return f"Processed {total_users} users, {successful_count} sent successfully, {failed_count} failed" except Exception as e: logger.error(f"Critical error in reminder process: {str(e)}") raise diff --git a/website/templates/website/reminder_settings.html b/website/templates/website/reminder_settings.html index 0049b4dacf..4ee5139d25 100644 --- a/website/templates/website/reminder_settings.html +++ b/website/templates/website/reminder_settings.html @@ -120,7 +120,7 @@

Daily Reminders

-
+
+ +
+ {% csrf_token %} + +
diff --git a/website/views/daily_reminders.py b/website/views/daily_reminders.py index 505ade2ec5..1b6bc6a3a5 100644 --- a/website/views/daily_reminders.py +++ b/website/views/daily_reminders.py @@ -70,20 +70,94 @@ def send_test_reminder(request): """Send a test reminder email to the user.""" if request.method == "POST": try: - # Create email message with the correct daily check-in link - email = EmailMessage( - subject="Test Daily Check-in Reminder", - body=f"""Hello {request.user.username}, - -This is a test reminder for your daily check-in. Please click the link below to complete your daily check-in: - -{request.build_absolute_uri('/add-sizzle-checkin/')} + # Get user's reminder settings + reminder_settings = ReminderSettings.objects.filter(user=request.user).first() + + # Get organization info + org_name = "" + org_info_html = "" + if hasattr(request.user, 'userprofile') and request.user.userprofile.team: + org_name = request.user.userprofile.team.name + org_info_html = f""" +
+

Organization: {org_name}

+
+ """ + + # Format reminder time if settings exist + reminder_time_str = "" + timezone_str = "" + time_info_html = "" + if reminder_settings: + reminder_time_str = reminder_settings.reminder_time.strftime("%I:%M %p") + timezone_str = reminder_settings.timezone + time_info_html = f""" +
+

Your Reminder Time: {reminder_time_str} ({timezone_str})

+
+ """ + + # Plain text body + plain_body = f"""Hello {request.user.username}, + +This is a test reminder for your daily check-in{f" for {org_name}" if org_name else ""}. + +{f"Reminder Time: {reminder_time_str} ({timezone_str})" if reminder_time_str else ""} + +Click here to check in: {request.build_absolute_uri('/add-sizzle-checkin/')} + +You can manage your reminder settings at: {request.build_absolute_uri('/reminder-settings/')} + +Regular check-ins help keep your team informed about your progress and any challenges you might be facing. + +Thank you for keeping your team updated! Best regards, -The BLT Team""", +The BLT Team""" + + # Create email message with HTML alternative + email = EmailMessage( + subject="Test Daily Check-in Reminder", + body=plain_body, from_email=settings.DEFAULT_FROM_EMAIL, to=[request.user.email], ) + + # Add HTML content + html_content = f""" + + +
+
+

⚠️ This is a test reminder

+
+

Daily Check-in Reminder

+

Hello {request.user.username},

+

This is a test reminder for your daily check-in{f" for {org_name}" if org_name else ""}! Please log in to update your status.

+ {org_info_html} + {time_info_html} +
+ + Check In Now + +
+

Regular check-ins help keep your team informed about your progress and any challenges you might be facing.

+
+

+ Manage your reminder settings +

+
+

Thank you for keeping your team updated!

+

Best regards,
The BLT Team

+
+ + + """ + + # Attach HTML alternative + email.content_subtype = "html" + email.body = html_content # Send the email email.send() From 7863ce12c3d11db953f25d5b0573f37e248adb77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:18:52 +0000 Subject: [PATCH 3/4] Apply code formatting with ruff and isort Co-authored-by: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> --- .../commands/cron_send_reminders.py | 8 ++++---- website/views/daily_reminders.py | 20 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/website/management/commands/cron_send_reminders.py b/website/management/commands/cron_send_reminders.py index 726834499f..e98c9996d6 100644 --- a/website/management/commands/cron_send_reminders.py +++ b/website/management/commands/cron_send_reminders.py @@ -105,7 +105,7 @@ def handle(self, *args, **options): time.sleep(delay) user = reminder_settings.user - + # Get organization info org_name = "" org_info_html = "" @@ -116,15 +116,15 @@ def handle(self, *args, **options):

Organization: {org_name}

""" - + # Format reminder time in user's timezone reminder_time_str = reminder_settings.reminder_time.strftime("%I:%M %p") timezone_str = reminder_settings.timezone - + # Create email message plain_body = f"""Hello {user.username}, -This is your daily check-in reminder for {org_name if org_name else 'your team'}. +This is your daily check-in reminder for {org_name if org_name else "your team"}. Reminder Time: {reminder_time_str} ({timezone_str}) diff --git a/website/views/daily_reminders.py b/website/views/daily_reminders.py index 1b6bc6a3a5..33112b803b 100644 --- a/website/views/daily_reminders.py +++ b/website/views/daily_reminders.py @@ -72,18 +72,18 @@ def send_test_reminder(request): try: # Get user's reminder settings reminder_settings = ReminderSettings.objects.filter(user=request.user).first() - + # Get organization info org_name = "" org_info_html = "" - if hasattr(request.user, 'userprofile') and request.user.userprofile.team: + if hasattr(request.user, "userprofile") and request.user.userprofile.team: org_name = request.user.userprofile.team.name org_info_html = f"""

Organization: {org_name}

""" - + # Format reminder time if settings exist reminder_time_str = "" timezone_str = "" @@ -96,7 +96,7 @@ def send_test_reminder(request):

Your Reminder Time: {reminder_time_str} ({timezone_str})

""" - + # Plain text body plain_body = f"""Hello {request.user.username}, @@ -104,9 +104,9 @@ def send_test_reminder(request): {f"Reminder Time: {reminder_time_str} ({timezone_str})" if reminder_time_str else ""} -Click here to check in: {request.build_absolute_uri('/add-sizzle-checkin/')} +Click here to check in: {request.build_absolute_uri("/add-sizzle-checkin/")} -You can manage your reminder settings at: {request.build_absolute_uri('/reminder-settings/')} +You can manage your reminder settings at: {request.build_absolute_uri("/reminder-settings/")} Regular check-ins help keep your team informed about your progress and any challenges you might be facing. @@ -122,7 +122,7 @@ def send_test_reminder(request): from_email=settings.DEFAULT_FROM_EMAIL, to=[request.user.email], ) - + # Add HTML content html_content = f""" @@ -137,7 +137,7 @@ def send_test_reminder(request): {org_info_html} {time_info_html}
- Check In Now @@ -145,7 +145,7 @@ def send_test_reminder(request):

Regular check-ins help keep your team informed about your progress and any challenges you might be facing.

- Manage your reminder settings + Manage your reminder settings

Thank you for keeping your team updated!

@@ -154,7 +154,7 @@ def send_test_reminder(request): """ - + # Attach HTML alternative email.content_subtype = "html" email.body = html_content From 1aeacab7b02d300b0371d1527ecbe6e1c1e0bcca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:24:10 +0000 Subject: [PATCH 4/4] Address code review feedback: fix email handling and throttling Co-authored-by: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> --- .../commands/cron_send_reminders.py | 13 ++++++++---- website/views/daily_reminders.py | 21 ++++++++----------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/website/management/commands/cron_send_reminders.py b/website/management/commands/cron_send_reminders.py index e98c9996d6..b009a45955 100644 --- a/website/management/commands/cron_send_reminders.py +++ b/website/management/commands/cron_send_reminders.py @@ -99,10 +99,15 @@ def handle(self, *args, **options): for i, (reminder_settings, profile) in enumerate(reminders_to_send, 1): try: # Add small delay between emails to avoid overwhelming the server - if i > 1 and i % 10 == 0: - delay = random.uniform(0.5, 2) - logger.info(f"Processed {i} emails, waiting {delay:.2f} seconds") + if i > 1: + # Small delay between each email + delay = random.uniform(0.1, 0.3) time.sleep(delay) + # Larger delay every 10 emails + if i % 10 == 0: + extra_delay = random.uniform(1, 2) + logger.info(f"Processed {i} emails, waiting {extra_delay:.2f} seconds") + time.sleep(extra_delay) user = reminder_settings.user @@ -124,7 +129,7 @@ def handle(self, *args, **options): # Create email message plain_body = f"""Hello {user.username}, -This is your daily check-in reminder for {org_name if org_name else "your team"}. +This is your daily check-in reminder{f" for {org_name}" if org_name else ""}. Reminder Time: {reminder_time_str} ({timezone_str}) diff --git a/website/views/daily_reminders.py b/website/views/daily_reminders.py index 33112b803b..a22ef9dd1a 100644 --- a/website/views/daily_reminders.py +++ b/website/views/daily_reminders.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.core.mail import EmailMessage +from django.core.mail import EmailMultiAlternatives from django.shortcuts import redirect, render from django.utils import timezone @@ -115,14 +115,6 @@ def send_test_reminder(request): Best regards, The BLT Team""" - # Create email message with HTML alternative - email = EmailMessage( - subject="Test Daily Check-in Reminder", - body=plain_body, - from_email=settings.DEFAULT_FROM_EMAIL, - to=[request.user.email], - ) - # Add HTML content html_content = f""" @@ -155,9 +147,14 @@ def send_test_reminder(request): """ - # Attach HTML alternative - email.content_subtype = "html" - email.body = html_content + # Create email with plain text body and HTML alternative + email = EmailMultiAlternatives( + subject="Test Daily Check-in Reminder", + body=plain_body, + from_email=settings.DEFAULT_FROM_EMAIL, + to=[request.user.email], + ) + email.attach_alternative(html_content, "text/html") # Send the email email.send()