HH/apps/projects/tasks.py
2026-07-11 19:24:28 +03:00

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}