117 lines
4.6 KiB
Python
117 lines
4.6 KiB
Python
"""Celery tasks for the QI Projects app."""
|
|
|
|
import logging
|
|
from datetime import datetime, time as datetime_time, timedelta
|
|
|
|
from celery import shared_task
|
|
from django.conf import settings
|
|
from django.utils import timezone
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
FIRST_REMINDER_HOURS_BEFORE = 24
|
|
SECOND_REMINDER_HOURS_BEFORE = 6
|
|
|
|
|
|
@shared_task
|
|
def send_qi_task_reminders():
|
|
"""Send deadline-approaching reminders for assigned QI tasks.
|
|
|
|
Runs every 15 minutes via Celery Beat. Sends a first reminder 24h before the
|
|
due date and a second reminder 6h before. Each reminder fires at most once per
|
|
task (deduped via `reminder_sent_at` / `second_reminder_sent_at`). Tasks with
|
|
no email-capable assignee or whose send fails are retried on subsequent runs.
|
|
"""
|
|
from apps.notifications.services import NotificationService
|
|
|
|
from .models import QIProjectTask
|
|
|
|
now = timezone.now()
|
|
tz = timezone.get_current_timezone()
|
|
site_url = getattr(settings, "SITE_URL", "http://localhost:8000").rstrip("/")
|
|
|
|
def _deadline(task):
|
|
# Interpret due_date (a date) as end-of-day in the current tz.
|
|
return timezone.make_aware(datetime.combine(task.due_date, datetime_time(23, 59, 59)), tz)
|
|
|
|
def _send(task, hours_before, label):
|
|
project_url = f"{site_url}/projects/{task.project.id}/"
|
|
message = (
|
|
f"Reminder ({label}): The task '{task.title}' in project "
|
|
f"'{task.project.name}' is due on {task.due_date.strftime('%Y-%m-%d')} "
|
|
f"({hours_before}h remaining)."
|
|
)
|
|
NotificationService.send_email(
|
|
email=task.assigned_to.user.email,
|
|
subject=f"QI Task Reminder: {task.title}",
|
|
message=message,
|
|
related_object=task,
|
|
metadata={
|
|
"notification_type": "qi_task_reminder",
|
|
"task_id": str(task.id),
|
|
"project_id": str(task.project.id),
|
|
"reminder": label,
|
|
"hours_before": hours_before,
|
|
},
|
|
notification_type="qi_task_reminder",
|
|
user=task.assigned_to.user,
|
|
)
|
|
logger.info(
|
|
f"QI task {label} reminder sent for task {task.id} "
|
|
f"(assignee {task.assigned_to_id}, due {task.due_date})"
|
|
)
|
|
|
|
first_count = 0
|
|
second_count = 0
|
|
|
|
# First reminders: due within 24h, not yet reminded
|
|
for task in QIProjectTask.objects.filter(
|
|
assigned_to__isnull=False,
|
|
status__in=["pending", "active"],
|
|
due_date__isnull=False,
|
|
reminder_sent_at__isnull=True,
|
|
project__is_template=False,
|
|
).select_related("assigned_to__user", "project"):
|
|
deadline = _deadline(task)
|
|
if not (deadline - timedelta(hours=FIRST_REMINDER_HOURS_BEFORE) <= now < deadline):
|
|
continue
|
|
user = task.assigned_to.user
|
|
if not user or not getattr(user, "email", None):
|
|
logger.warning(f"QI task {task.id} assignee has no email; skipping first reminder.")
|
|
continue
|
|
try:
|
|
_send(task, FIRST_REMINDER_HOURS_BEFORE, "first")
|
|
task.reminder_sent_at = now
|
|
task.save(update_fields=["reminder_sent_at"])
|
|
first_count += 1
|
|
except Exception as e:
|
|
logger.warning(f"Failed to send first QI reminder for task {task.id}: {e}")
|
|
|
|
# Second reminders: due within 6h, first sent >=1h ago, not yet second-reminded
|
|
for task in QIProjectTask.objects.filter(
|
|
assigned_to__isnull=False,
|
|
status__in=["pending", "active"],
|
|
due_date__isnull=False,
|
|
second_reminder_sent_at__isnull=True,
|
|
reminder_sent_at__isnull=False,
|
|
reminder_sent_at__lt=now - timedelta(hours=1),
|
|
project__is_template=False,
|
|
).select_related("assigned_to__user", "project"):
|
|
deadline = _deadline(task)
|
|
if not (deadline - timedelta(hours=SECOND_REMINDER_HOURS_BEFORE) <= now < deadline):
|
|
continue
|
|
user = task.assigned_to.user
|
|
if not user or not getattr(user, "email", None):
|
|
logger.warning(f"QI task {task.id} assignee has no email; skipping second reminder.")
|
|
continue
|
|
try:
|
|
_send(task, SECOND_REMINDER_HOURS_BEFORE, "second")
|
|
task.second_reminder_sent_at = now
|
|
task.save(update_fields=["second_reminder_sent_at"])
|
|
second_count += 1
|
|
except Exception as e:
|
|
logger.warning(f"Failed to send second QI reminder for task {task.id}: {e}")
|
|
|
|
logger.info(f"QI task reminders: {first_count} first, {second_count} second")
|
|
return {"first_reminder_count": first_count, "second_reminder_count": second_count}
|