Internal
Public Access
Critical fix: Task now runs every hour instead of once at 6 AM UTC. This ensures users in all timezones receive their email at 6 AM local time. Changes: - Run task every hour instead of once daily - Check if it's 6-7 AM in user's timezone (1 hour window) - Track sent emails in Notification model to prevent duplicates - Add 'daily_email' notification type Without this fix, users in timezones where 6 AM UTC is not morning would never receive their daily email. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
183 lines
6.1 KiB
Python
183 lines
6.1 KiB
Python
"""
|
|
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_daily_task_email():
|
|
"""
|
|
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 tasks.models import Task
|
|
|
|
User = get_user_model()
|
|
|
|
# Get all users who have email notifications enabled
|
|
users = User.objects.filter(
|
|
email_notifications=True,
|
|
email_verified=True,
|
|
is_active=True
|
|
)
|
|
|
|
emails_sent = 0
|
|
current_utc_hour = timezone.now().hour
|
|
|
|
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')
|
|
|
|
user_local_time = timezone.now().astimezone(user_tz)
|
|
user_local_hour = user_local_time.hour
|
|
|
|
# Only send if it's between 6-7 AM in the user's timezone
|
|
# (gives 1 hour window, task runs every hour)
|
|
if not (6 <= user_local_hour < 7):
|
|
continue
|
|
|
|
# Get today's date in user's timezone
|
|
today = user_local_time.date()
|
|
|
|
# Check if we already sent an email today
|
|
from .models import Notification
|
|
already_sent_today = Notification.objects.filter(
|
|
user=user,
|
|
notification_type='daily_email',
|
|
created_at__date=today
|
|
).exists()
|
|
|
|
if already_sent_today:
|
|
continue
|
|
|
|
# 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')
|
|
|
|
# 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')
|
|
|
|
# Only send email if there are tasks
|
|
if not tasks_due_today.exists() and not overdue_tasks.exists():
|
|
continue
|
|
|
|
# Prepare email content
|
|
subject = f"Your tasks for {today.strftime('%A, %B %d, %Y')}"
|
|
|
|
# Create plain text message
|
|
message_lines = [
|
|
f"Good morning! Here are your tasks for today:\n"
|
|
]
|
|
|
|
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())
|
|
|
|
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.")
|
|
|
|
message = '\n'.join(message_lines)
|
|
|
|
# Send email
|
|
try:
|
|
send_mail(
|
|
subject=subject,
|
|
message=message,
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
recipient_list=[user.email],
|
|
fail_silently=False,
|
|
)
|
|
|
|
# Record that we sent the email (prevents duplicates)
|
|
Notification.objects.create(
|
|
user=user,
|
|
notification_type='daily_email',
|
|
title=subject,
|
|
message=f"Daily email sent with {tasks_due_today.count()} tasks due today and {overdue_tasks.count()} overdue tasks"
|
|
)
|
|
|
|
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 daily as a safety net to catch any tasks that weren't
|
|
automatically handled when marked as completed.
|
|
"""
|
|
from tasks.models import Task
|
|
|
|
# Find completed recurring tasks from the last 48 hours
|
|
yesterday = timezone.now() - timedelta(days=2)
|
|
|
|
completed_recurring_tasks = Task.objects.filter(
|
|
status='completed',
|
|
recurrence__in=['daily', 'weekly', 'biweekly', 'monthly', 'yearly', 'custom'],
|
|
completed_at__gte=yesterday
|
|
).exclude(recurrence='none')
|
|
|
|
tasks_created = 0
|
|
for task in completed_recurring_tasks:
|
|
# Check if a next recurrence already exists
|
|
next_due_date = task.calculate_next_due_date()
|
|
if next_due_date:
|
|
# Check if we already created this recurrence
|
|
existing = Task.objects.filter(
|
|
user=task.user,
|
|
title=task.title,
|
|
due_date=next_due_date,
|
|
recurrence=task.recurrence,
|
|
status='pending'
|
|
).exists()
|
|
|
|
if not existing:
|
|
# Create the next recurrence
|
|
new_task = task.create_next_recurrence()
|
|
if new_task:
|
|
tasks_created += 1
|
|
logger.info(f"Created recurring task: {new_task.title} (due: {new_task.due_date})")
|
|
|
|
if tasks_created > 0:
|
|
logger.info(f"Created {tasks_created} recurring task instances")
|
|
|
|
return tasks_created
|