From 4054a2f78cad318b2d45022e07529c7f7b2dac21 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 26 Feb 2020 15:53:25 +0200 Subject: [PATCH] Implement item update and deletion. Deletion is essentially an update with "isDeletion" set to True. --- ...0013_collectionitemrevision_is_deletion.py | 18 +++++++++++++++ .../migrations/0014_auto_20200226_1322.py | 18 +++++++++++++++ .../migrations/0015_auto_20200226_1349.py | 18 +++++++++++++++ django_etesync/models.py | 3 ++- django_etesync/serializers.py | 22 +++++++++++++++++-- django_etesync/views.py | 8 ++----- 6 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 django_etesync/migrations/0013_collectionitemrevision_is_deletion.py create mode 100644 django_etesync/migrations/0014_auto_20200226_1322.py create mode 100644 django_etesync/migrations/0015_auto_20200226_1349.py diff --git a/django_etesync/migrations/0013_collectionitemrevision_is_deletion.py b/django_etesync/migrations/0013_collectionitemrevision_is_deletion.py new file mode 100644 index 0000000..27f4953 --- /dev/null +++ b/django_etesync/migrations/0013_collectionitemrevision_is_deletion.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.3 on 2020-02-26 13:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_etesync', '0012_auto_20200220_2038'), + ] + + operations = [ + migrations.AddField( + model_name='collectionitemrevision', + name='is_deletion', + field=models.BooleanField(default=False), + ), + ] diff --git a/django_etesync/migrations/0014_auto_20200226_1322.py b/django_etesync/migrations/0014_auto_20200226_1322.py new file mode 100644 index 0000000..1937015 --- /dev/null +++ b/django_etesync/migrations/0014_auto_20200226_1322.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.3 on 2020-02-26 13:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_etesync', '0013_collectionitemrevision_is_deletion'), + ] + + operations = [ + migrations.RenameField( + model_name='collectionitemrevision', + old_name='is_deletion', + new_name='isDeletion', + ), + ] diff --git a/django_etesync/migrations/0015_auto_20200226_1349.py b/django_etesync/migrations/0015_auto_20200226_1349.py new file mode 100644 index 0000000..896619d --- /dev/null +++ b/django_etesync/migrations/0015_auto_20200226_1349.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.3 on 2020-02-26 13:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_etesync', '0014_auto_20200226_1322'), + ] + + operations = [ + migrations.AlterField( + model_name='collectionitemrevision', + name='current', + field=models.BooleanField(blank=True, db_index=True, default=True, null=True), + ), + ] diff --git a/django_etesync/models.py b/django_etesync/models.py index 18743fd..dda081f 100644 --- a/django_etesync/models.py +++ b/django_etesync/models.py @@ -80,7 +80,8 @@ class CollectionItemRevision(models.Model): item = models.ForeignKey(CollectionItem, related_name='revisions', on_delete=models.CASCADE) chunks = models.ManyToManyField(CollectionItemChunk, related_name='items') hmac = models.CharField(max_length=50, blank=False, null=False) - current = models.BooleanField(db_index=True, default=True) + current = models.BooleanField(db_index=True, default=True, blank=True, null=True) + isDeletion = models.BooleanField(default=False) class Meta: unique_together = ('item', 'current') diff --git a/django_etesync/serializers.py b/django_etesync/serializers.py index 0e5b355..b3f7254 100644 --- a/django_etesync/serializers.py +++ b/django_etesync/serializers.py @@ -15,6 +15,7 @@ import base64 from django.contrib.auth import get_user_model +from django.db import transaction from rest_framework import serializers from . import models @@ -74,7 +75,7 @@ class CollectionItemRevisionBaseSerializer(serializers.ModelSerializer): class Meta: model = models.CollectionItemRevision - fields = ('version', 'encryptionKey', 'chunks', 'hmac') + fields = ('version', 'encryptionKey', 'chunks', 'hmac', 'isDeletion') class CollectionItemRevisionSerializer(CollectionItemRevisionBaseSerializer): @@ -111,12 +112,29 @@ class CollectionItemRevisionInlineSerializer(CollectionItemRevisionBaseSerialize class CollectionItemSerializer(serializers.ModelSerializer): - content = CollectionItemRevisionSerializer(read_only=True, many=False) + content = CollectionItemRevisionSerializer(many=False) class Meta: model = models.CollectionItem fields = ('uid', 'content') + def update(self, instance, validated_data): + """Function that's called when this serializer is meant to update an item""" + revision_data = validated_data.pop('content') + + with transaction.atomic(): + # We don't have to use select_for_update here because the unique constraint on current guards against + # the race condition. But it's a good idea because it'll lock and wait rather than fail. + current_revision = instance.revisions.filter(current=True).select_for_update().first() + current_revision.current = None + current_revision.save() + + chunks = revision_data.pop('chunks') + revision = models.CollectionItemRevision.objects.create(**revision_data, item=instance) + revision.chunks.set(chunks) + + return instance + class CollectionItemInlineSerializer(CollectionItemSerializer): content = CollectionItemRevisionInlineSerializer(read_only=True, many=False) diff --git a/django_etesync/views.py b/django_etesync/views.py index 2e91261..b90b62e 100644 --- a/django_etesync/views.py +++ b/django_etesync/views.py @@ -90,7 +90,7 @@ class CollectionViewSet(BaseViewSet): class CollectionItemViewSet(BaseViewSet): - allowed_methods = ['GET', 'POST'] + allowed_methods = ['GET', 'POST', 'PUT'] permission_classes = BaseViewSet.permission_classes queryset = CollectionItem.objects.all() serializer_class = CollectionItemSerializer @@ -133,11 +133,7 @@ class CollectionItemViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, collection_uid=None, uid=None): - # FIXME: implement - return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) - - def update(self, request, collection_uid=None, uid=None): - # FIXME: implement, or should it be implemented elsewhere? + # We can't have destroy because we need to get data from the user (in the body) such as hmac. return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) def partial_update(self, request, collection_uid=None, uid=None):