import logging from django.core.files.base import ContentFile from django.db import transaction from django.db.models.signals import post_delete, post_save, pre_save from django.dispatch import receiver from common.image_utils import compress_avatar from .models import Task, TaskActivity, TaskAttachment, TaskBoard logger = logging.getLogger(__name__) def _delete_file_after_commit(field_file): if not field_file or not field_file.name: return storage = field_file.storage name = field_file.name transaction.on_commit( lambda: storage.delete(name) if storage.exists(name) else None ) @receiver(pre_save, sender=TaskBoard) def compress_and_cleanup_task_board_avatar(sender, instance, **kwargs): old_avatar = None if instance.pk: old_avatar = ( sender.objects.filter(pk=instance.pk) .only("avatar") .values_list("avatar", flat=True) .first() ) new_avatar = instance.avatar if new_avatar and not getattr(new_avatar, "_committed", True): try: compressed = compress_avatar(new_avatar.read()) if compressed: new_avatar.save( new_avatar.name, ContentFile(compressed), save=False, ) except Exception: new_avatar.seek(0) logger.exception( "Task board avatar compression failed", extra={ "event": "task.board.avatar.compression_failed", "task_board_id": instance.pk, }, ) new_avatar_name = new_avatar.name if new_avatar else None if old_avatar and old_avatar != new_avatar_name: storage = instance._meta.get_field("avatar").storage transaction.on_commit( lambda: storage.delete(old_avatar) if storage.exists(old_avatar) else None ) @receiver(post_delete, sender=TaskBoard) def delete_task_board_avatar(sender, instance, **kwargs): _delete_file_after_commit(instance.avatar) @receiver(post_save, sender=Task) def ensure_task_core_participants(sender, instance, **kwargs): participant_ids = [instance.created_by_id] if instance.assignee_id: participant_ids.append(instance.assignee_id) instance.participants.add(*participant_ids) @receiver(pre_save, sender=Task) def remember_task_completion_state(sender, instance, **kwargs): if not instance.pk: instance._skill_was_completed = False instance._skill_previous_completed_at = None return previous_completed_at = ( sender.objects.filter(pk=instance.pk) .values_list( "completed_at", flat=True, ) .first() ) instance._skill_previous_completed_at = previous_completed_at instance._skill_was_completed = previous_completed_at is not None @receiver(post_save, sender=Task) def sync_task_skill_experience(sender, instance, **kwargs): was_completed = bool(getattr(instance, "_skill_was_completed", False)) is_completed = instance.completed_at is not None if was_completed == is_completed: return task_id = instance.id actor = getattr(instance, "_skill_completion_actor", None) previous_completed_at = getattr( instance, "_skill_previous_completed_at", None, ) def sync_after_commit(): from employees.services.skill_progress import ( award_task_skill_experience, revoke_task_skill_experience, ) from employees.services.task_execution_skill import ( capture_task_execution_cycle, reopen_task_execution_cycle, ) if is_completed: award_task_skill_experience(task_id) capture_task_execution_cycle(task_id) else: revoke_task_skill_experience(task_id, actor=actor) reopen_task_execution_cycle(task_id, previous_completed_at) transaction.on_commit(sync_after_commit) @receiver(post_delete, sender=TaskAttachment) def delete_task_attachment_file(sender, instance, **kwargs): _delete_file_after_commit(instance.file) @receiver(post_save, sender=TaskActivity) def dispatch_task_automation_event(sender, instance, created, **kwargs): if not created: return activity_id = instance.id def dispatch(): from tasks.tasks import process_task_automation_event_task process_task_automation_event_task.delay(activity_id) transaction.on_commit(dispatch)