""" Celery tasks for notifications. """ import logging from celery import shared_task from django.utils import timezone from datetime import timedelta logger = logging.getLogger(__name__) @shared_task def send_due_reminders(): """ 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. """ from django.contrib.auth import get_user_model from users.models import DeviceToken User = get_user_model() try: user = User.objects.get(id=user_id) except User.DoesNotExist: return if not user.push_notifications: return tokens = DeviceToken.objects.filter(user=user, is_active=True) 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 @shared_task def send_fcm_notification(token, title, body, data=None): """Send FCM notification to Android device.""" from django.conf import settings # Only send if FCM is configured if not getattr(settings, 'FCM_CREDENTIALS_PATH', None): return try: import firebase_admin from firebase_admin import credentials, messaging # Initialize Firebase if not already done if not firebase_admin._apps: cred = credentials.Certificate(settings.FCM_CREDENTIALS_PATH) firebase_admin.initialize_app(cred) message = messaging.Message( notification=messaging.Notification( title=title, body=body, ), data=data or {}, token=token, ) messaging.send(message) except Exception as e: # Log error but don't fail logger.error(f"FCM notification failed: {e}") @shared_task def send_web_push_notification(subscription_info, title, body, data=None): """Send Web Push notification.""" from django.conf import settings vapid_private_key = getattr(settings, 'VAPID_PRIVATE_KEY', None) vapid_email = getattr(settings, 'VAPID_ADMIN_EMAIL', None) 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 ) @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 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) 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 # 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 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