From: Radek Czajka Date: Fri, 1 Aug 2025 14:53:48 +0000 (+0200) Subject: Bookmarks sync X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/078017198692badda854eb70f7a5431112aed007?ds=sidebyside;hp=c07a8fb779699c186417d5435e5b935ef340a3a5 Bookmarks sync --- diff --git a/src/bookmarks/api/views.py b/src/bookmarks/api/views.py index 87314c651..38408c1da 100644 --- a/src/bookmarks/api/views.py +++ b/src/bookmarks/api/views.py @@ -15,13 +15,17 @@ from api.fields import AbsoluteURLField class BookmarkSerializer(serializers.ModelSerializer): - book = serializers.SlugRelatedField(queryset=catalogue.models.Book.objects.all(), slug_field='slug') + book = serializers.SlugRelatedField( + queryset=catalogue.models.Book.objects.all(), slug_field='slug', + required=False + ) href = AbsoluteURLField(view_name='api_bookmark', view_args=['uuid']) - timestamp = serializers.IntegerField() + timestamp = serializers.IntegerField(required=False) + location = serializers.CharField(required=False) class Meta: model = models.Bookmark - fields = ['book', 'anchor', 'note', 'href', 'uuid', 'location', 'timestamp'] + fields = ['book', 'anchor', 'note', 'href', 'uuid', 'location', 'timestamp', 'deleted'] read_only_fields = ['uuid'] diff --git a/src/bookmarks/migrations/0003_bookmark_deleted_bookmark_reported_timestamp_and_more.py b/src/bookmarks/migrations/0003_bookmark_deleted_bookmark_reported_timestamp_and_more.py new file mode 100644 index 000000000..8e9f32c3e --- /dev/null +++ b/src/bookmarks/migrations/0003_bookmark_deleted_bookmark_reported_timestamp_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.0.8 on 2025-08-01 14:35 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookmarks', '0002_quote'), + ] + + operations = [ + migrations.AddField( + model_name='bookmark', + name='deleted', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='bookmark', + name='reported_timestamp', + field=models.DateTimeField(default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='bookmark', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/src/bookmarks/models.py b/src/bookmarks/models.py index 234d0519d..583cd55c7 100644 --- a/src/bookmarks/models.py +++ b/src/bookmarks/models.py @@ -1,24 +1,65 @@ import uuid +from django.apps import apps from django.db import models +from django.utils.timezone import now +from social.syncable import Syncable -class Bookmark(models.Model): +class Bookmark(Syncable, models.Model): uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) user = models.ForeignKey('auth.User', models.CASCADE) book = models.ForeignKey('catalogue.Book', models.CASCADE) anchor = models.CharField(max_length=100, blank=True) created_at = models.DateTimeField(auto_now_add=True) note = models.TextField(blank=True) + updated_at = models.DateTimeField(auto_now=True) + reported_timestamp = models.DateTimeField(default=now) + deleted = models.BooleanField(default=False) + + syncable_fields = [ + 'deleted', 'note', + ] def __str__(self): return str(self.uuid) + @classmethod + def create_from_data(cls, user, data): + if data.get('location'): + return cls.get_by_location(user, data['location'], create=True) + elif data.get('book') and data.get('anchor'): + return cls.objects.create(user=user, book=data['book'], anchor=data['anchor']) + @property def timestamp(self): - return self.created_at.timestamp() + return self.updated_at.timestamp() def location(self): return f'{self.book.slug}/{self.anchor}' + + @classmethod + def get_by_location(cls, user, location, create=False): + Book = apps.get_model('catalogue', 'Book') + try: + slug, anchor = location.split('/') + except: + return None + instance = cls.objects.filter( + user=user, + book__slug=slug, + anchor=anchor + ).first() + if instance is None and create: + try: + book = Book.objects.get(slug=slug) + except Book.DoesNotExist: + return None + instance = cls.objects.create( + user=user, + book=book, + anchor=anchor + ) + return instance def get_for_json(self): return { diff --git a/src/social/api/views.py b/src/social/api/views.py index ea8b1287e..cad07df05 100644 --- a/src/social/api/views.py +++ b/src/social/api/views.py @@ -464,8 +464,9 @@ class BookmarkSyncView(SyncView): sync_id_field = 'uuid' sync_id_serializer_field = 'uuid' - def get_queryset_for_ts(self, timestamp): - return self.model.objects.filter( - user=self.request.user, - created_at__gt=timestamp - ).order_by('created_at') + def get_instance(self, user, data): + ret = super().get_instance(user, data) + if ret is None: + if data.get('location'): + ret = self.model.get_by_location(user, data['location']) + return ret diff --git a/src/social/models.py b/src/social/models.py index 0b366fc29..2692538be 100644 --- a/src/social/models.py +++ b/src/social/models.py @@ -16,6 +16,7 @@ from django.utils.timezone import now from catalogue.models import Book from catalogue.utils import get_random_hash from wolnelektury.utils import cached_render, clear_cached_renders +from .syncable import Syncable class BannerGroup(models.Model): @@ -207,44 +208,6 @@ class UserConfirmation(models.Model): ).send() - -class Syncable: - @classmethod - def sync(cls, user, instance, data): - ts = data.get('timestamp') - if ts is None: - ts = now() - else: - ts = datetime.fromtimestamp(ts, tz=utc) - - if instance is not None: - if ts and ts < instance.reported_timestamp: - return - - if instance is None: - if data.get('deleted'): - return - instance = cls.create_from_data(user, data) - if instance is None: - return - - instance.reported_timestamp = ts - for f in cls.syncable_fields: - if f in data: - setattr(instance, f, data[f]) - - instance.save() - return instance - - @property - def timestamp(self): - return self.updated_at.timestamp() - - @classmethod - def create_from_data(cls, user, data): - raise NotImplementedError - - class Progress(Syncable, models.Model): user = models.ForeignKey(User, models.CASCADE) book = models.ForeignKey('catalogue.Book', models.CASCADE) diff --git a/src/social/syncable.py b/src/social/syncable.py new file mode 100644 index 000000000..789cdf840 --- /dev/null +++ b/src/social/syncable.py @@ -0,0 +1,41 @@ +from datetime import datetime +from django.utils.timezone import now +from pytz import utc + + + +class Syncable: + @classmethod + def sync(cls, user, instance, data): + ts = data.get('timestamp') + if ts is None: + ts = now() + else: + ts = datetime.fromtimestamp(ts, tz=utc) + + if instance is not None: + if ts and ts < instance.reported_timestamp: + return + + if instance is None: + if data.get('deleted'): + return + instance = cls.create_from_data(user, data) + if instance is None: + return + + instance.reported_timestamp = ts + for f in cls.syncable_fields: + if f in data: + setattr(instance, f, data[f]) + + instance.save() + return instance + + @property + def timestamp(self): + return self.updated_at.timestamp() + + @classmethod + def create_from_data(cls, user, data): + raise NotImplementedError