from django.db.models.signals import m2m_changed, post_delete, post_save, pre_save from django.dispatch import receiver from apps.blog.models import Comment, Like, Post, SavedPost from apps.events.models import Registration from apps.gamification.services import ( desired_effect, has_source_history, invalidate_post_engagement, sync_comment, sync_effect, sync_like, sync_post_engagement_for_credited_authors, sync_post_publication, sync_registration_attendance, sync_save, sync_source_effects, ) from apps.users.models import User def _post_is_public(post): return Post.StatusChoices.PUBLISHED == post.status and not post.is_deleted def _credited_author_ids(post): return {post.author_id, *post.writers.values_list("id", flat=True)} @receiver(pre_save, sender=Post) def remember_post_visibility(sender, instance, **kwargs): if not instance.pk: instance._gamification_was_public = False return old = Post.all_objects.filter(pk=instance.pk).values("status", "is_deleted").first() instance._gamification_was_public = bool( old and old["status"] == Post.StatusChoices.PUBLISHED and not old["is_deleted"] ) @receiver(post_save, sender=Post) def score_post_visibility(sender, instance, created, **kwargs): was_public = getattr(instance, "_gamification_was_public", False) is_public = _post_is_public(instance) if is_public and not was_public: sync_post_publication(instance, valid=True) sync_post_engagement_for_credited_authors(instance) elif was_public and not is_public: sync_post_publication(instance, valid=False) invalidate_post_engagement(instance) @receiver(post_delete, sender=Post) def reverse_deleted_post(sender, instance, **kwargs): sync_source_effects("blog_post", instance.pk, [], reason="post was deleted") @receiver(m2m_changed, sender=Post.writers.through) def score_credited_writer_changes(sender, instance, action, reverse, pk_set, **kwargs): if reverse or not _post_is_public(instance): return writer_ids = set(pk_set or getattr(instance, "_gamification_writer_ids_before_clear", set())) if action in {"pre_clear"}: instance._gamification_writer_ids_before_clear = set(instance.writers.values_list("id", flat=True)) return if action not in {"post_add", "post_remove", "post_clear"}: return for user in User.objects.filter(pk__in=writer_ids): if action != "post_add" and user.pk == instance.author_id: continue effect = { **desired_effect(user, "post", "author"), "source_type": "blog_post", "source_id": str(instance.pk), } sync_effect(effect, valid=action == "post_add", reason="post coauthor credit was removed") sync_post_engagement_for_credited_authors(instance) instance._gamification_writer_ids_before_clear = set() @receiver(pre_save, sender=Comment) def remember_comment_approval(sender, instance, **kwargs): if not instance.pk: instance._gamification_was_approved = False return old = Comment.all_objects.filter(pk=instance.pk).values("is_approved", "is_hidden", "is_deleted").first() instance._gamification_was_approved = bool( old and old["is_approved"] and not old["is_hidden"] and not old["is_deleted"] ) @receiver(post_save, sender=Comment) def score_approved_comment(sender, instance, created, **kwargs): became_approved = not getattr(instance, "_gamification_was_approved", False) and instance.is_approved sync_comment(instance, allow_new=created or became_approved) @receiver(post_delete, sender=Comment) def reverse_deleted_comment(sender, instance, **kwargs): sync_source_effects("blog_comment", instance.pk, [], reason="comment was deleted") @receiver(post_save, sender=Like) def score_like(sender, instance, created, **kwargs): sync_like(instance, allow_new=created) @receiver(post_delete, sender=Like) def reverse_like(sender, instance, **kwargs): sync_source_effects("blog_like", instance.pk, [], reason="like was removed") @receiver(post_save, sender=SavedPost) def score_saved_post(sender, instance, created, **kwargs): sync_save(instance, allow_new=created) @receiver(post_delete, sender=SavedPost) def reverse_saved_post(sender, instance, **kwargs): sync_source_effects("blog_save", instance.pk, [], reason="saved post was removed") @receiver(post_save, sender=Registration) def score_registration_attendance(sender, instance, **kwargs): sync_registration_attendance(instance)