""" 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( timezone.datetime.combine(task.due_date, timezone.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 )