Files
KeepItGoingServer/notifications/tasks.py
T
Keith SmithandClaude Sonnet 4.5 c8ffa615f0 Fix recurring task duplication bug
Prevent process_recurring_tasks from creating duplicate tasks for dates that already have completed instances. Previously only checked for pending tasks, causing old completed recurring tasks to reappear as pending when subsequent instances went overdue.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 16:52:06 -07:00

183 lines
6.2 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
# Check for any task (pending OR completed) to avoid duplicates
existing = Task.objects.filter(
user=task.user,
title=task.title,
due_date=next_due_date,
recurrence=task.recurrence
).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