""" Models for Social app DataSignalManager captures create/update/(soft/hard)delete/add/remove from Journal app, and generate Activity objects, ActivityManager generates chronological view for user and, in future, ActivityStreams """ from typing import Type from django.conf import settings from django.db import models from django.db.models import Q from django.db.models.signals import post_save, pre_delete from django.utils import timezone from loguru import logger from journal.models import ( Collection, Comment, FeaturedCollection, Like, Piece, Review, ShelfMember, UserOwnedObjectMixin, ) from users.models import APIdentity class ActivityTemplate(models.TextChoices): MarkItem = "mark_item" ReviewItem = "review_item" CreateCollection = "create_collection" LikeCollection = "like_collection" FeatureCollection = "feature_collection" CommentChildItem = "comment_child_item" class LocalActivity(models.Model, UserOwnedObjectMixin): owner = models.ForeignKey(APIdentity, on_delete=models.CASCADE) visibility = models.PositiveSmallIntegerField(default=0) # type: ignore template = models.CharField( blank=False, choices=ActivityTemplate.choices, max_length=50 ) action_object = models.ForeignKey(Piece, on_delete=models.CASCADE) created_time = models.DateTimeField(default=timezone.now, db_index=True) class Meta: index_together = [ ["owner", "created_time"], ] def __str__(self): return f"Activity [{self.owner}:{self.template}:{self.action_object}]" class ActivityManager: def __init__(self, owner: APIdentity): self.owner = owner def get_timeline(self, before_time=None): following = [x for x in self.owner.following if x not in self.owner.muting] q = Q(owner_id__in=following, visibility__lt=2) | Q(owner=self.owner) if before_time: q = q & Q(created_time__lt=before_time) return ( LocalActivity.objects.filter(q) .order_by("-created_time") .prefetch_related("action_object", "owner") ) # .select_related() https://github.com/django-polymorphic/django-polymorphic/pull/531 @staticmethod def get_manager_for_user(user): return ActivityManager(user) class DataSignalManager: processors = {} @staticmethod def save_handler(sender, instance, created, **kwargs): processor_class = DataSignalManager.processors.get(instance.__class__) if processor_class: processor = processor_class(instance) if created: if hasattr(processor, "created"): processor.created() elif hasattr(processor, "updated"): processor.updated() @staticmethod def delete_handler(sender, instance, **kwargs): processor_class = DataSignalManager.processors.get(instance.__class__) if processor_class: processor = processor_class(instance) if hasattr(processor, "deleted"): processor.deleted() @staticmethod def add_handler_for_model(model): if settings.DISABLE_MODEL_SIGNAL: logger.warning( f"{model.__name__} are not being indexed with DISABLE_MODEL_SIGNAL configuration" ) return post_save.connect(DataSignalManager.save_handler, sender=model) pre_delete.connect(DataSignalManager.delete_handler, sender=model) @staticmethod def register(processor): DataSignalManager.add_handler_for_model(processor.model) DataSignalManager.processors[processor.model] = processor return processor class DefaultActivityProcessor: model: Type[Piece] template: ActivityTemplate def __init__(self, action_object): self.action_object = action_object def created(self): params = { "owner": self.action_object.owner, "visibility": self.action_object.visibility, "template": self.template, "action_object": self.action_object, "created_time": self.action_object.created_time, } LocalActivity.objects.create(**params) def updated(self): activity = LocalActivity.objects.filter( action_object=self.action_object ).first() if not activity: self.created() elif ( activity.visibility != self.action_object.visibility or activity.created_time != activity.action_object.created_time ): activity.visibility = self.action_object.visibility activity.created_time = activity.action_object.created_time activity.save() @DataSignalManager.register class MarkProcessor(DefaultActivityProcessor): model = ShelfMember template = ActivityTemplate.MarkItem @DataSignalManager.register class ReviewProcessor(DefaultActivityProcessor): model = Review template = ActivityTemplate.ReviewItem @DataSignalManager.register class CollectionProcessor(DefaultActivityProcessor): model = Collection template = ActivityTemplate.CreateCollection @DataSignalManager.register class LikeCollectionProcessor(DefaultActivityProcessor): model = Like template = ActivityTemplate.LikeCollection def created(self): if isinstance(self.action_object.target, Collection): super().created() def updated(self): if isinstance(self.action_object.target, Collection): super().updated() @DataSignalManager.register class FeaturedCollectionProcessor(DefaultActivityProcessor): model = FeaturedCollection template = ActivityTemplate.FeatureCollection @DataSignalManager.register class CommentChildItemProcessor(DefaultActivityProcessor): model = Comment template = ActivityTemplate.CommentChildItem def created(self): if self.action_object.item.class_name in ["podcastepisode", "tvepisode"]: super().created() def updated(self): if self.action_object.item.class_name in ["podcastepisode", "tvepisode"]: super().updated() def reset_social_visibility_for_user(owner: APIdentity, visibility: int): LocalActivity.objects.filter(owner=owner).update(visibility=visibility)