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']
--- /dev/null
+# 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),
+ ),
+ ]
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 {
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
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):
).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)
--- /dev/null
+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