From: Radek Czajka Date: Tue, 20 Oct 2026 07:57:54 +0000 (+0200) Subject: Quotes API X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain Quotes API --- diff --git a/src/bookmarks/api/urls.py b/src/bookmarks/api/urls.py index f61b3263f..bd6b2efdb 100644 --- a/src/bookmarks/api/urls.py +++ b/src/bookmarks/api/urls.py @@ -6,4 +6,7 @@ urlpatterns = [ path('bookmarks/', views.BookmarksView.as_view()), path('bookmarks/book//', views.BookBookmarksView.as_view()), path('bookmarks//', views.BookmarkView.as_view(), name='api_bookmark'), + + path('quotes/', views.QuotesView.as_view()), + path('quotes//', views.QuoteView.as_view(), name='api_quote'), ] diff --git a/src/bookmarks/api/views.py b/src/bookmarks/api/views.py index 6aeb82de8..1280ad552 100644 --- a/src/bookmarks/api/views.py +++ b/src/bookmarks/api/views.py @@ -117,3 +117,66 @@ class BookmarkView(RetrieveUpdateDestroyAPIView): instance.deleted = True instance.updated_at = now() instance.save() + + + +class QuoteSerializer(serializers.ModelSerializer): + book_slug = serializers.SlugRelatedField( + queryset=catalogue.models.Book.objects.all(), slug_field='slug', + write_only=True, + required=False + ) + book = BookSerializer2(read_only=True) + href = AbsoluteURLField(view_name='api_quote', view_args=['uuid']) + timestamp = serializers.IntegerField(required=False) + + class Meta: + model = models.Quote + fields = ['book_slug', 'book', + 'start_elem', 'start_offset', 'end_elem', 'end_offset', + 'href', 'uuid', 'timestamp', 'deleted'] + read_only_fields = ['uuid', 'mode'] + + def create(self, validated_data): + book = validated_data.pop('book_slug', None) + if book is not None: + validated_data['book'] = book + return super().create(validated_data) + + def update(self, instance, validated_data): + book = validated_data.pop('book_slug', None) + if book is not None: + validated_data['book'] = book + return super().update(instance, validated_data) + + +@never_cache +class QuotesView(ListCreateAPIView): + permission_classes = [IsAuthenticated] + serializer_class = QuoteSerializer + + def get_queryset(self): + return self.request.user.quote_set.all() + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + +class QuoteView(RetrieveUpdateDestroyAPIView): + permission_classes = [IsAuthenticatedOrReadOnly] + serializer_class = QuoteSerializer + lookup_field = 'uuid' + + def get_queryset(self): + if self.request.method in SAFE_METHODS: + q = Q(deleted=False) + if self.request.user.is_authenticated: + q |= Q(user=self.request.user) + return models.Quote.objects.filter(q) + else: + return self.request.user.quote_set.all() + + def perform_destroy(self, instance): + instance.deleted = True + instance.updated_at = now() + instance.save() diff --git a/src/bookmarks/migrations/0005_quote_deleted_quote_reported_timestamp_and_more.py b/src/bookmarks/migrations/0005_quote_deleted_quote_reported_timestamp_and_more.py new file mode 100644 index 000000000..fac3404a4 --- /dev/null +++ b/src/bookmarks/migrations/0005_quote_deleted_quote_reported_timestamp_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0.8 on 2026-10-20 07:54 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookmarks', '0004_bookmark_audio_timestamp_bookmark_mode_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='quote', + name='deleted', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='quote', + name='reported_timestamp', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='quote', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/src/bookmarks/models.py b/src/bookmarks/models.py index 098a8b004..ca53cd6c3 100644 --- a/src/bookmarks/models.py +++ b/src/bookmarks/models.py @@ -115,7 +115,7 @@ class Bookmark(Syncable, models.Model): } -class Quote(models.Model): +class Quote(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) @@ -125,10 +125,21 @@ class Quote(models.Model): start_offset = models.IntegerField(null=True, blank=True) end_offset = models.IntegerField(null=True, blank=True) text = 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', + ] def __str__(self): return str(self.uuid) + @property + def timestamp(self): + return self.updated_at.timestamp() + def get_for_json(self): return { 'uuid': self.uuid, @@ -151,6 +162,3 @@ def from_path(elem, path): n = path.pop(0) elem = list(child_nodes(elem))[n] return elem - - - diff --git a/src/social/api/urls2.py b/src/social/api/urls2.py index 8f1dc150c..400571a64 100644 --- a/src/social/api/urls2.py +++ b/src/social/api/urls2.py @@ -32,6 +32,7 @@ urlpatterns = [ path('sync/userlist/', views.UserListSyncView.as_view()), path('sync/userlistitem/', views.UserListItemSyncView.as_view()), path('sync/bookmark/', views.BookmarkSyncView.as_view()), + path('sync/quote/', views.QuoteSyncView.as_view()), ] diff --git a/src/social/api/views.py b/src/social/api/views.py index 35af9a8ca..8e09e81c5 100644 --- a/src/social/api/views.py +++ b/src/social/api/views.py @@ -21,7 +21,7 @@ from social.utils import likes from social import models import bookmarks.models from . import serializers -from bookmarks.api.views import BookmarkSerializer +from bookmarks.api.views import BookmarkSerializer, QuoteSerializer @@ -452,3 +452,11 @@ class BookmarkSyncView(SyncView): if data.get('location'): ret = self.model.get_by_location(user, data['location']) return ret + + +class QuoteSyncView(SyncView): + model = bookmarks.models.Quote + serializer_class = QuoteSerializer + + sync_id_field = 'uuid' + sync_id_serializer_field = 'uuid'