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'),
]
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()
--- /dev/null
+# 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),
+ ),
+ ]
}
-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)
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,
n = path.pop(0)
elem = list(child_nodes(elem))[n]
return elem
-
-
-
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()),
]
from social import models
import bookmarks.models
from . import serializers
-from bookmarks.api.views import BookmarkSerializer
+from bookmarks.api.views import BookmarkSerializer, QuoteSerializer
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'