From: Radek Czajka Date: Tue, 8 Jul 2025 15:29:52 +0000 (+0200) Subject: basic sync view X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/a84167ce03ffdca5a9ff74f5e3c2f40dc05fbbf4?ds=sidebyside basic sync view --- diff --git a/src/social/api/urls2.py b/src/social/api/urls2.py index d2d6245b5..039a6fea7 100644 --- a/src/social/api/urls2.py +++ b/src/social/api/urls2.py @@ -21,6 +21,8 @@ urlpatterns = [ path('progress//', views.ProgressView.as_view()), path('progress//text/', views.TextProgressView.as_view()), path('progress//audio/', views.AudioProgressView.as_view()), + + path('sync/', views.SyncView.as_view()), ] diff --git a/src/social/api/views.py b/src/social/api/views.py index 867a05a54..c58efd1c9 100644 --- a/src/social/api/views.py +++ b/src/social/api/views.py @@ -1,6 +1,8 @@ # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Wolne Lektury. See NOTICE for more information. # +from datetime import datetime +from pytz import utc from django.http import Http404 from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, DestroyAPIView, get_object_or_404 from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly @@ -276,3 +278,61 @@ class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView): serializer.instance.last_mode = 'audio' serializer.save() + + +class SyncSerializer(serializers.Serializer): + timestamp = serializers.IntegerField() + type = serializers.CharField() + id = serializers.CharField() + + def to_representation(self, instance): + rep = super().to_representation(instance) + rep['object'] = instance['object'].data + return rep + + def to_internal_value(self, data): + ret = super().to_internal_value(data) + ret['object'] = data['object'] + return ret + + +class SyncView(ListAPIView): + permission_classes = [IsAuthenticated] + serializer_class = SyncSerializer + + def get_queryset(self): + try: + timestamp = int(self.request.GET.get('ts')) + except: + timestamp = 0 + + timestamp = datetime.fromtimestamp(timestamp, tz=utc) + + data = [] + for p in models.Progress.objects.filter( + user=self.request.user, + updated_at__gt=timestamp).order_by('updated_at'): + data.append({ + 'timestamp': p.updated_at.timestamp(), + 'type': 'progress', + 'id': p.book.slug, + 'object': ProgressSerializer( + p, context={'request': self.request} + ) if not p.deleted else None + }) + return data + + def post(self, request): + data = request.data + for item in data: + ser = SyncSerializer(data=item) + ser.is_valid(raise_exception=True) + d = ser.validated_data + if d['type'] == 'progress': + models.Progress.sync( + user=request.user, + slug=d['id'], + ts=datetime.fromtimestamp(d['timestamp'], tz=utc), + data=d['object'] + ) + return Response() diff --git a/src/social/migrations/0019_progress_deleted.py b/src/social/migrations/0019_progress_deleted.py new file mode 100644 index 000000000..71b1fab53 --- /dev/null +++ b/src/social/migrations/0019_progress_deleted.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.8 on 2025-07-08 14:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('social', '0018_progress'), + ] + + operations = [ + migrations.AddField( + model_name='progress', + name='deleted', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/social/models.py b/src/social/models.py index c7096dd55..c63879b5f 100644 --- a/src/social/models.py +++ b/src/social/models.py @@ -208,6 +208,7 @@ class Progress(models.Model): book = models.ForeignKey('catalogue.Book', models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + deleted = models.BooleanField(default=False) last_mode = models.CharField(max_length=64, choices=[ ('text', 'text'), ('audio', 'audio'), @@ -224,6 +225,18 @@ class Progress(models.Model): class Meta: unique_together = [('user', 'book')] + @classmethod + def sync(cls, user, slug, ts, data): + obj, _created = cls.objects.get_or_create(user=user, book__slug=slug) + if _created or obj.updated_at < ts: + if data is not None: + obj.deleted = False + for k, v in data.items(): + setattr(obj, k, v) + else: + obj.deleted = True + obj.save() + def save(self, *args, **kwargs): audio_l = self.book.get_audio_length() if self.text_anchor: