Simplify notification system to daily email only

Replace complex notification system with a simple daily email:
- Send ONE email per day between 6-9 AM in user's timezone
- Show tasks due today and overdue tasks
- Only send if user has email_notifications enabled
- Remove all push notification logic
- Keep recurring task processor (runs daily at midnight)

This makes notifications much simpler and less intrusive while
still providing value to users.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Keith Smith
2026-01-09 09:30:46 -07:00
co-authored by Claude Sonnet 4.5
parent 842b958b43
commit 03cfdea42d
2 changed files with 88 additions and 217 deletions
+4 -8
View File
@@ -22,17 +22,13 @@ app.autodiscover_tasks()
# Celery Beat schedule for periodic tasks
app.conf.beat_schedule = {
'send-due-reminders': {
'task': 'notifications.tasks.send_due_reminders',
'schedule': crontab(minute='*/5'), # Every 5 minutes
},
'check-overdue-tasks': {
'task': 'notifications.tasks.check_overdue_tasks',
'schedule': crontab(hour=8, minute=0), # Daily at 8 AM
'send-daily-task-email': {
'task': 'notifications.tasks.send_daily_task_email',
'schedule': crontab(hour=6, minute=0), # Daily at 6 AM UTC
},
'process-recurring-tasks': {
'task': 'notifications.tasks.process_recurring_tasks',
'schedule': crontab(minute=0), # Every hour
'schedule': crontab(hour=0, minute=0), # Daily at midnight
},
}
+84 -209
View File
@@ -5,252 +5,128 @@ Celery tasks for notifications.
import logging
from celery import shared_task
from django.utils import timezone
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.conf import settings
from datetime import timedelta
from zoneinfo import ZoneInfo
logger = logging.getLogger(__name__)
@shared_task
def send_due_reminders():
def send_daily_task_email():
"""
Check for tasks due soon and send reminders.
Runs every 5 minutes via Celery Beat.
"""
from tasks.models import Task
from users.models import DeviceToken
from .models import Notification, ScheduledReminder
now = timezone.now()
# Find scheduled reminders that need to be sent
reminders = ScheduledReminder.objects.filter(
remind_at__lte=now,
is_sent=False,
task__status__in=['pending', 'in_progress']
).select_related('task', 'task__user')
for reminder in reminders:
task = reminder.task
user = task.user
# Create notification
Notification.objects.create(
user=user,
notification_type='reminder',
title=f'Reminder: {task.title}',
message=f'Task "{task.title}" is due soon.',
task=task,
)
# Send push notifications
send_push_to_user.delay(
user_id=str(user.id),
title=f'Reminder: {task.title}',
body=f'Task is due soon.',
data={'task_id': str(task.id)}
)
# Mark as sent
reminder.is_sent = True
reminder.sent_at = now
reminder.save()
@shared_task
def check_overdue_tasks():
"""
Check for overdue tasks and notify users.
Runs daily.
"""
from tasks.models import Task
from .models import Notification
today = timezone.now().date()
# Find overdue tasks that haven't been notified today
overdue_tasks = Task.objects.filter(
due_date__lt=today,
status__in=['pending', 'in_progress']
).select_related('user')
for task in overdue_tasks:
# Check if we already sent an overdue notification today
existing = Notification.objects.filter(
user=task.user,
task=task,
notification_type='overdue',
created_at__date=today
).exists()
if not existing:
Notification.objects.create(
user=task.user,
notification_type='overdue',
title=f'Overdue: {task.title}',
message=f'Task "{task.title}" is overdue.',
task=task,
)
send_push_to_user.delay(
user_id=str(task.user.id),
title=f'Overdue: {task.title}',
body='This task is overdue.',
data={'task_id': str(task.id)}
)
@shared_task
def send_push_to_user(user_id, title, body, data=None):
"""
Send push notification to all user devices.
Send daily email to users with their tasks due today.
Runs once per day and sends to users in their local morning time.
"""
from django.contrib.auth import get_user_model
from users.models import DeviceToken
from tasks.models import Task
User = get_user_model()
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
return
# Get all users who have email notifications enabled
users = User.objects.filter(
email_notifications=True,
email_verified=True,
is_active=True
)
if not user.push_notifications:
return
emails_sent = 0
current_utc_hour = timezone.now().hour
tokens = DeviceToken.objects.filter(user=user, is_active=True)
for user in users:
# Convert current UTC time to user's local time
try:
user_tz = ZoneInfo(user.timezone)
except Exception:
user_tz = ZoneInfo('UTC')
for token in tokens:
if token.platform == 'android':
send_fcm_notification.delay(token.token, title, body, data)
elif token.platform == 'web':
send_web_push_notification.delay(token.token, title, body, data)
elif token.platform == 'desktop':
# Desktop notifications are handled via WebSocket
pass
user_local_time = timezone.now().astimezone(user_tz)
user_local_hour = user_local_time.hour
# Only send if it's between 6-9 AM in the user's timezone
if not (6 <= user_local_hour < 9):
continue
@shared_task
def send_fcm_notification(token, title, body, data=None):
"""Send FCM notification to Android device."""
from django.conf import settings
# Get today's date in user's timezone
today = user_local_time.date()
# Only send if FCM is configured
if not getattr(settings, 'FCM_CREDENTIALS_PATH', None):
return
# Get tasks due today
tasks_due_today = Task.objects.filter(
user=user,
due_date=today,
status__in=['pending', 'in_progress']
).order_by('due_time', 'priority', 'title')
try:
import firebase_admin
from firebase_admin import credentials, messaging
# Get overdue tasks
overdue_tasks = Task.objects.filter(
user=user,
due_date__lt=today,
status__in=['pending', 'in_progress']
).order_by('due_date', 'due_time', 'priority', 'title')
# Initialize Firebase if not already done
if not firebase_admin._apps:
cred = credentials.Certificate(settings.FCM_CREDENTIALS_PATH)
firebase_admin.initialize_app(cred)
# Only send email if there are tasks
if not tasks_due_today.exists() and not overdue_tasks.exists():
continue
message = messaging.Message(
notification=messaging.Notification(
title=title,
body=body,
),
data=data or {},
token=token,
)
# Prepare email content
subject = f"Your tasks for {today.strftime('%A, %B %d, %Y')}"
messaging.send(message)
# Create plain text message
message_lines = [
f"Good morning! Here are your tasks for today:\n"
]
except Exception as e:
# Log error but don't fail
logger.error(f"FCM notification failed: {e}")
if overdue_tasks.exists():
message_lines.append(f"\n⚠️ OVERDUE TASKS ({overdue_tasks.count()}):")
for task in overdue_tasks[:10]: # Limit to 10
due_str = task.due_date.strftime('%b %d')
message_lines.append(f"{task.title} (due {due_str})")
if overdue_tasks.count() > 10:
message_lines.append(f" ... and {overdue_tasks.count() - 10} more")
if tasks_due_today.exists():
message_lines.append(f"\n📅 DUE TODAY ({tasks_due_today.count()}):")
for task in tasks_due_today:
time_str = task.due_time.strftime('%I:%M %p') if task.due_time else ''
priority_icon = {'high': '🔴', 'medium': '🟡', 'low': '🟢'}.get(task.priority, '')
message_lines.append(f"{priority_icon} {task.title} {time_str}".strip())
@shared_task
def send_web_push_notification(subscription_info, title, body, data=None):
"""Send Web Push notification."""
from django.conf import settings
message_lines.append(f"\n\nView all tasks: https://{settings.SITE_DOMAIN}")
message_lines.append("\nYou can change your email preferences in your profile settings.")
vapid_private_key = getattr(settings, 'VAPID_PRIVATE_KEY', None)
vapid_email = getattr(settings, 'VAPID_ADMIN_EMAIL', None)
message = '\n'.join(message_lines)
if not vapid_private_key or not vapid_email:
return
try:
from pywebpush import webpush, WebPushException
import json
webpush(
subscription_info=json.loads(subscription_info),
data=json.dumps({
'title': title,
'body': body,
'data': data or {}
}),
vapid_private_key=vapid_private_key,
vapid_claims={'sub': f'mailto:{vapid_email}'}
)
except Exception as e:
logger.error(f"Web push notification failed: {e}")
@shared_task
def schedule_task_reminder(task_id):
"""
Schedule a reminder for a task based on its due date and user preferences.
Called when a task is created or updated.
"""
from tasks.models import Task
from .models import ScheduledReminder
try:
task = Task.objects.get(id=task_id)
except Task.DoesNotExist:
return
# Remove existing scheduled reminders for this task
ScheduledReminder.objects.filter(task=task, is_sent=False).delete()
# Only schedule if task has a reminder time or due date
if task.reminder_at:
ScheduledReminder.objects.create(
task=task,
remind_at=task.reminder_at
)
elif task.due_date:
# Default reminder based on user preference
reminder_minutes = task.user.default_reminder_minutes
if task.due_time:
from datetime import datetime
due_datetime = datetime.combine(task.due_date, task.due_time)
due_datetime = timezone.make_aware(due_datetime)
else:
# Default to 9 AM on due date
due_datetime = timezone.make_aware(
datetime.combine(task.due_date, datetime.min.time())
).replace(hour=9)
remind_at = due_datetime - timedelta(minutes=reminder_minutes)
if remind_at > timezone.now():
ScheduledReminder.objects.create(
task=task,
remind_at=remind_at
# Send email
try:
send_mail(
subject=subject,
message=message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user.email],
fail_silently=False,
)
emails_sent += 1
logger.info(f"Sent daily task email to {user.email}")
except Exception as e:
logger.error(f"Failed to send daily email to {user.email}: {e}")
logger.info(f"Daily task email job completed. Sent {emails_sent} emails.")
return emails_sent
@shared_task
def process_recurring_tasks():
"""
Process completed recurring tasks and create next instances.
This runs periodically as a safety net to catch any tasks that weren't
This runs daily as a safety net to catch any tasks that weren't
automatically handled when marked as completed.
Runs every hour via Celery Beat.
"""
from tasks.models import Task
# Find completed recurring tasks that don't have a next instance created yet
# We look for tasks completed in the last 24 hours to avoid reprocessing old tasks
yesterday = timezone.now() - timedelta(days=1)
# Find completed recurring tasks from the last 48 hours
yesterday = timezone.now() - timedelta(days=2)
completed_recurring_tasks = Task.objects.filter(
status='completed',
@@ -261,7 +137,6 @@ def process_recurring_tasks():
tasks_created = 0
for task in completed_recurring_tasks:
# Check if a next recurrence already exists
# Look for pending tasks with the same title, user, and recurrence pattern
next_due_date = task.calculate_next_due_date()
if next_due_date:
# Check if we already created this recurrence