new data model: collection mark; local timeline

This commit is contained in:
Your Name 2022-12-20 11:32:44 -05:00
parent 72347011b4
commit 7ee5c7d587
5 changed files with 112 additions and 129 deletions

View file

@ -15,10 +15,7 @@ User .. Activity
class Activity {
+User owner
+int visibility
+Enum action_type
+Piece action_object
+Item target
+Bool is_viewable
}
Activity .. Piece
Activity .. Item
@ -38,6 +35,18 @@ Activity data may be used for:
However, 2 is currently implemented separately via `ShelfLogManager` in `journal` app, because users may want to change these records manually.
Local Timeline
--------------
| Local Timeline Activities | action object class |
| ------------------------- | ------------------- |
| Add an Item to Shelf | ShelfMember |
| Create a Collection | Collection |
| Like a Collection | Like |
| Create a Review | Review |
Activity Streams
----------------
These are list of activities should be either shown in the site or delivered as ActivityStreams or both:
- `Add` / `Remove` an *Item* to / from a *List*:

View file

@ -39,6 +39,10 @@ class Content(Piece):
abstract = True
class Like(Piece):
target = models.ForeignKey(Piece, on_delete=models.CASCADE, related_name='likes')
class Note(Content):
pass
@ -127,8 +131,8 @@ Item.rating = property(Rating.get_rating_for_item)
Item.rating_count = property(Rating.get_rating_count_for_item)
class Reply(Content):
reply_to_content = models.ForeignKey(Piece, on_delete=models.PROTECT, related_name='replies')
class Reply(Piece):
reply_to_content = models.ForeignKey(Piece, on_delete=models.SET_NULL, related_name='replies', null=True)
title = models.CharField(max_length=500, null=True)
body = MarkdownxField()
pass

View file

@ -9,6 +9,7 @@ from movies.models import MovieMark, MovieReview
from music.models import AlbumMark, AlbumReview
from games.models import GameMark, GameReview
from collection.models import Collection as Legacy_Collection
from collection.models import CollectionMark as Legacy_CollectionMark
from catalog.common import *
from catalog.models import *
from catalog.sites import *
@ -88,13 +89,9 @@ class Command(BaseCommand):
cls.objects.all().delete()
def collection(self, options):
qs = Legacy_Collection.objects.all().filter(owner__is_active=True).order_by('id')
if options['id']:
if options['maxid']:
qs = qs.filter(id__gte=int(options['id']), id__lte=int(options['maxid']))
else:
qs = qs.filter(id=int(options['id']))
collection_map = {}
with transaction.atomic():
qs = Legacy_Collection.objects.all().filter(owner__is_active=True).order_by('id')
for entity in tqdm(qs):
c = Collection.objects.create(
owner_id=entity.owner_id,
@ -104,6 +101,7 @@ class Command(BaseCommand):
created_time=entity.created_time,
edited_time=entity.edited_time,
)
collection_map[entity.id] = c.id
c.catalog_item.cover = entity.cover
c.catalog_item.save()
for citem in entity.collectionitem_list:
@ -120,6 +118,14 @@ class Command(BaseCommand):
else:
# TODO convert song to album
print(f'{c.owner} {c.id} {c.title} {citem.item} were skipped')
qs = Legacy_CollectionMark.objects.all().filter(owner__is_active=True).order_by('id')
for entity in tqdm(qs):
Like.objects.create(
owner_id=entity.owner_id,
target_id=collection_map[entity.collection_id],
created_time=entity.created_time,
edited_time=entity.edited_time,
)
def review(self, options):
for typ in [GameReview, AlbumReview, BookReview, MovieReview]:

View file

@ -1,7 +1,7 @@
"""
Models for Social app
DataSignalManager captures create/update/(soft/hard)delete from Journal app, and generate Activity objects,
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
"""
@ -20,36 +20,32 @@ from django.conf import settings
_logger = logging.getLogger(__name__)
class ActionType(models.TextChoices):
Create = 'create'
Delete = 'delete'
Update = 'update'
Add = 'add'
Remove = 'remove'
Like = 'like'
Undo_Like = 'undo_like'
Announce = 'announce'
Undo_Announce = 'undo_announce'
Follow = 'follow'
Undo_Follow = 'undo_follow'
Flag = 'flag'
Move = 'move'
Accept = 'accept'
Reject = 'reject'
Block = 'block'
Undo_Block = 'undo_block'
class ActivityTemplate(models.TextChoices):
"""
"""
MarkItem = 'mark_item'
ReviewItem = 'review_item'
CreateCollection = 'create_collection'
LikeCollection = 'like_collection'
class LocalActivity(models.Model, UserOwnedObjectMixin):
owner = models.ForeignKey(User, on_delete=models.CASCADE)
visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only
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 ActivityManager:
def __init__(self, user):
self.owner = user
def get_viewable_activities(self, before_time=None):
def get_timeline(self, before_time=None):
q = Q(owner_id__in=self.owner.following, visibility__lt=2) | Q(owner=self.owner)
q = q & Q(is_viewable=True)
if before_time:
q = q & Q(created_time__lt=before_time)
return Activity.objects.filter(q)
return LocalActivity.objects.filter(q)
@staticmethod
def get_manager_for_user(user):
@ -60,87 +56,26 @@ User.activity_manager = cached_property(ActivityManager.get_manager_for_user)
User.activity_manager.__set_name__(User, 'activity_manager')
class Activity(models.Model, UserOwnedObjectMixin):
owner = models.ForeignKey(User, on_delete=models.PROTECT)
visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only
action_type = models.CharField(blank=False, choices=ActionType.choices, max_length=50)
action_object = models.ForeignKey(Piece, on_delete=models.SET_NULL, null=True)
is_viewable = models.BooleanField(default=True) # if viewable in local time line, otherwise it's event only for s2s
# action_uid = TODO
@property
def target(self):
return get_attself.action_object
@property
def action_class(self):
return self.action_object.__class__.__name__
def __str__(self):
return f'{self.id}:{self.action_type}:{self.action_object}:{self.is_viewable}'
class DefaultSignalProcessor():
def __init__(self, action_object):
self.action_object = action_object
def activity_viewable(self, action_type):
return action_type == ActionType.Create and bool(getattr(self.action_object, 'attached_to', None) is None)
def created(self):
activity = Activity.objects.create(owner=self.action_object.owner, visibility=self.action_object.visibility, action_object=self.action_object, action_type=ActionType.Create, is_viewable=self.activity_viewable(ActionType.Create))
return activity
def updated(self):
create_activity = Activity.objects.filter(owner=self.action_object.owner, action_object=self.action_object, action_type=ActionType.Create).first()
action_type = ActionType.Update if create_activity else ActionType.Create
is_viewable = self.activity_viewable(action_type)
return Activity.objects.create(owner=self.action_object.owner, visibility=self.action_object.visibility, action_object=self.action_object, action_type=action_type, is_viewable=is_viewable)
def deleted(self):
create_activity = Activity.objects.filter(owner=self.action_object.owner, action_object=self.action_object, action_type=ActionType.Create).first()
if create_activity:
create_activity.is_viewable = False
create_activity.save()
else:
_logger.warning(f'unable to find create activity for {self.action_object}')
# FIXME action_object=self.action_object causing issues in test when hard delete, the bare minimum is to save id of the actual object that ActivityPub requires
return Activity.objects.create(owner=self.action_object.owner, visibility=self.action_object.visibility, action_object=None, action_type=ActionType.Delete, is_viewable=self.activity_viewable(ActionType.Delete))
class UnhandledSignalProcessor(DefaultSignalProcessor):
def created(self):
_logger.warning(f'unhandled created signal for {self.action_object}')
def updated(self):
_logger.warning(f'unhandled updated signal for {self.action_object}')
def deleted(self):
_logger.warning(f'unhandled deleted signal for {self.action_object}')
class DataSignalManager:
processors = {}
@staticmethod
def save_handler(sender, instance, created, **kwargs):
processor_class = DataSignalManager.processors.get(instance.__class__)
if not processor_class:
processor_class = GenericSignalProcessor
if processor_class:
processor = processor_class(instance)
if created:
if hasattr(processor, 'created'):
processor.created()
elif getattr(instance, 'is_deleted', False):
processor.deleted()
else:
elif hasattr(processor, 'updated'):
processor.updated()
@staticmethod
def delete_handler(sender, instance, **kwargs):
processor_class = DataSignalManager.processors.get(instance.__class__)
if not processor_class:
processor_class = GenericSignalProcessor
if processor_class:
processor = processor_class(instance)
if hasattr(processor, 'deleted'):
processor.deleted()
@staticmethod
@ -156,29 +91,58 @@ class DataSignalManager:
return processor
class DefaultActivityProcessor:
model = None
template = None
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,
}
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:
activity.visibility = self.action_object.visibility
activity.save()
@DataSignalManager.register
class MarkProcessor(DefaultSignalProcessor):
class MarkProcessor(DefaultActivityProcessor):
model = ShelfMember
# @DataSignalManager.register
# class ReplyProcessor(DefaultSignalProcessor):
# model = Reply
# def activity_viewable(self):
# return False
# @DataSignalManager.register
# class RatingProcessor(DefaultSignalProcessor):
# model = Rating
template = ActivityTemplate.MarkItem
@DataSignalManager.register
class ReviewProcessor(DefaultSignalProcessor):
class ReviewProcessor(DefaultActivityProcessor):
model = Review
template = ActivityTemplate.ReviewItem
@DataSignalManager.register
class CollectionProcessor(DefaultSignalProcessor):
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, Collection):
super.created()
def updated(self):
if isinstance(self.action_object, Collection):
super.update()

View file

@ -17,38 +17,38 @@ class SocialTest(TestCase):
def test_timeline(self):
# alice see 0 activity in timeline in the beginning
timeline = self.alice.activity_manager.get_viewable_activities()
timeline = self.alice.activity_manager.get_timeline()
self.assertEqual(len(timeline), 0)
# 1 activity after adding first book to shelf
self.alice.shelf_manager.move_item(self.book1, ShelfType.WISHLIST, visibility=1)
timeline = self.alice.activity_manager.get_viewable_activities()
timeline = self.alice.activity_manager.get_timeline()
self.assertEqual(len(timeline), 1)
# 2 activities after adding second book to shelf
self.alice.shelf_manager.move_item(self.book2, ShelfType.WISHLIST)
timeline = self.alice.activity_manager.get_viewable_activities()
timeline = self.alice.activity_manager.get_timeline()
self.assertEqual(len(timeline), 2)
# 2 activities after change first mark
self.alice.shelf_manager.move_item(self.book1, ShelfType.PROGRESS)
timeline = self.alice.activity_manager.get_viewable_activities()
timeline = self.alice.activity_manager.get_timeline()
self.assertEqual(len(timeline), 2)
# bob see 0 activity in timeline in the beginning
timeline2 = self.bob.activity_manager.get_viewable_activities()
timeline2 = self.bob.activity_manager.get_timeline()
self.assertEqual(len(timeline2), 0)
# bob follows alice, see 2 activities
self.bob.mastodon_following = ['Alice@MySpace']
self.alice.mastodon_follower = ['Bob@KKCity']
self.bob.following = self.bob.get_following_ids()
timeline2 = self.bob.activity_manager.get_viewable_activities()
timeline2 = self.bob.activity_manager.get_timeline()
self.assertEqual(len(timeline2), 2)
# alice:3 bob:2 after alice adding second book to shelf as private
self.alice.shelf_manager.move_item(self.movie, ShelfType.WISHLIST, visibility=2)
timeline = self.alice.activity_manager.get_viewable_activities()
timeline = self.alice.activity_manager.get_timeline()
self.assertEqual(len(timeline), 3)
timeline2 = self.bob.activity_manager.get_viewable_activities()
timeline2 = self.bob.activity_manager.get_timeline()
self.assertEqual(len(timeline2), 2)