new data model: collection mark; local timeline
This commit is contained in:
parent
72347011b4
commit
7ee5c7d587
5 changed files with 112 additions and 129 deletions
|
@ -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*:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]:
|
||||
|
|
172
social/models.py
172
social/models.py
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue