""" 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