""" Signals for QI Projects. - Sends a notification when a task is assigned. - Keeps `team_members` in sync with the champion + manager of each linked department whenever the `departments` M2M changes. """ import logging from django.db.models.signals import m2m_changed, post_save from django.dispatch import receiver from apps.projects.models import QIProject logger = logging.getLogger(__name__) @receiver(post_save, sender="projects.QIProjectTask") def notify_task_assignment(sender, instance, created, **kwargs): """Send an in-app notification when a QI task is assigned to a staff member.""" if not instance.assigned_to: return # Only notify on new assignments (creation with an assignee) if not created: return user = getattr(instance.assigned_to, "user", None) if not user: return try: from apps.notifications.services import create_in_app_notification create_in_app_notification( user=user, title=f"New QI Task: {instance.title}", message=f"You have been assigned a task in project '{instance.project.name}'.", notification_type="qi_task_assigned", action_url=f"/projects/{instance.project.id}/", ) except Exception as e: logger.warning(f"Failed to send QI task notification: {e}") @receiver(m2m_changed, sender=QIProject.departments.through) def sync_team_on_departments_change(sender, instance, action, **kwargs): """Rebuild the derived `team_members` set whenever the project's departments change. Fires on `post_add` / `post_remove` / `post_clear` so that programmatic, admin, and form-driven edits all stay consistent. The sync is idempotent. """ if action not in ("post_add", "post_remove", "post_clear"): return if not instance.pk: return try: instance.sync_team_members_from_departments() except Exception as e: # defensive: never break an M2M write on sync failure logger.warning(f"Failed to sync QI team members for project {instance.pk}: {e}")