Quotes API master
authorRadek Czajka <rczajka@rczajka.pl>
Tue, 20 Oct 2026 07:57:54 +0000 (09:57 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Tue, 20 Oct 2026 07:57:54 +0000 (09:57 +0200)
src/bookmarks/api/urls.py
src/bookmarks/api/views.py
src/bookmarks/migrations/0005_quote_deleted_quote_reported_timestamp_and_more.py [new file with mode: 0644]
src/bookmarks/models.py
src/social/api/urls2.py
src/social/api/views.py

index f61b326..bd6b2ef 100644 (file)
@@ -6,4 +6,7 @@ urlpatterns = [
     path('bookmarks/', views.BookmarksView.as_view()),
     path('bookmarks/book/<slug:book>/', views.BookBookmarksView.as_view()),
     path('bookmarks/<uuid:uuid>/', views.BookmarkView.as_view(), name='api_bookmark'),
+
+    path('quotes/', views.QuotesView.as_view()),
+    path('quotes/<uuid:uuid>/', views.QuoteView.as_view(), name='api_quote'),
 ]
index 6aeb82d..1280ad5 100644 (file)
@@ -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 (file)
index 0000000..fac3404
--- /dev/null
@@ -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),
+        ),
+    ]
index 098a8b0..ca53cd6 100644 (file)
@@ -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
-            
-            
-                
index 8f1dc15..400571a 100644 (file)
@@ -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()),
 ]
 
 
index 35af9a8..8e09e81 100644 (file)
@@ -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'