From 66e5062461398b0cbec6f4fa28b73f92577fe73e Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 12 Mar 2020 21:02:27 +0200 Subject: [PATCH] Collection: add content support. --- .../migrations/0027_collection_mainitem.py | 19 ++++ .../migrations/0028_auto_20200312_1819.py | 24 +++++ .../migrations/0029_auto_20200312_1849.py | 31 +++++++ .../migrations/0030_auto_20200312_1859.py | 19 ++++ django_etesync/models.py | 11 ++- django_etesync/serializers.py | 87 +++++++++++-------- 6 files changed, 154 insertions(+), 37 deletions(-) create mode 100644 django_etesync/migrations/0027_collection_mainitem.py create mode 100644 django_etesync/migrations/0028_auto_20200312_1819.py create mode 100644 django_etesync/migrations/0029_auto_20200312_1849.py create mode 100644 django_etesync/migrations/0030_auto_20200312_1859.py diff --git a/django_etesync/migrations/0027_collection_mainitem.py b/django_etesync/migrations/0027_collection_mainitem.py new file mode 100644 index 0000000..b420d8f --- /dev/null +++ b/django_etesync/migrations/0027_collection_mainitem.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.3 on 2020-03-12 14:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_etesync', '0026_collectionitemrevision_meta'), + ] + + operations = [ + migrations.AddField( + model_name='collection', + name='mainItem', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='of_collection', to='django_etesync.CollectionItem'), + ), + ] diff --git a/django_etesync/migrations/0028_auto_20200312_1819.py b/django_etesync/migrations/0028_auto_20200312_1819.py new file mode 100644 index 0000000..6d76499 --- /dev/null +++ b/django_etesync/migrations/0028_auto_20200312_1819.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.3 on 2020-03-12 18:19 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_etesync', '0027_collection_mainitem'), + ] + + operations = [ + migrations.AlterField( + model_name='collectionitem', + name='encryptionKey', + field=models.BinaryField(editable=True, null=True), + ), + migrations.AlterField( + model_name='collectionitem', + name='uid', + field=models.CharField(db_index=True, max_length=44, null=True, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='[a-zA-Z0-9]')]), + ), + ] diff --git a/django_etesync/migrations/0029_auto_20200312_1849.py b/django_etesync/migrations/0029_auto_20200312_1849.py new file mode 100644 index 0000000..165b405 --- /dev/null +++ b/django_etesync/migrations/0029_auto_20200312_1849.py @@ -0,0 +1,31 @@ +# Generated by Django 3.0.3 on 2020-03-12 18:49 + +from django.db import migrations +from django_etesync.serializers import generate_rev_uid + + +def add_collection_main_item(apps, schema_editor): + Collection = apps.get_model('django_etesync', 'Collection') + CollectionItem = apps.get_model('django_etesync', 'CollectionItem') + CollectionItemRevision = apps.get_model('django_etesync', 'CollectionItemRevision') + + for col in Collection.objects.all(): + main_item = CollectionItem.objects.create(uid=None, encryptionKey=None, version=col.version, collection=col) + col.mainItem = main_item + col.save() + + CollectionItemRevision.objects.create( + uid=generate_rev_uid(), + hmac='hmac-hash', + item=main_item) + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_etesync', '0028_auto_20200312_1819'), + ] + + operations = [ + migrations.RunPython(add_collection_main_item), + ] diff --git a/django_etesync/migrations/0030_auto_20200312_1859.py b/django_etesync/migrations/0030_auto_20200312_1859.py new file mode 100644 index 0000000..fe8050a --- /dev/null +++ b/django_etesync/migrations/0030_auto_20200312_1859.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.3 on 2020-03-12 18:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_etesync', '0029_auto_20200312_1849'), + ] + + operations = [ + migrations.AlterField( + model_name='collection', + name='mainItem', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='of_collection', to='django_etesync.CollectionItem'), + ), + ] diff --git a/django_etesync/models.py b/django_etesync/models.py index 02a173c..6c30b00 100644 --- a/django_etesync/models.py +++ b/django_etesync/models.py @@ -29,6 +29,7 @@ class Collection(models.Model): max_length=44, validators=[UidValidator]) version = models.PositiveSmallIntegerField() owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + mainItem = models.OneToOneField('CollectionItem', related_name='of_collection', on_delete=models.PROTECT) class Meta: unique_together = ('uid', 'owner') @@ -36,19 +37,23 @@ class Collection(models.Model): def __str__(self): return self.uid + @cached_property + def content(self): + return self.mainItem.content + class CollectionItem(models.Model): - uid = models.CharField(db_index=True, blank=False, null=False, + uid = models.CharField(db_index=True, blank=False, null=True, max_length=44, validators=[UidValidator]) collection = models.ForeignKey(Collection, related_name='items', on_delete=models.CASCADE) version = models.PositiveSmallIntegerField() - encryptionKey = models.BinaryField(editable=True, blank=False, null=False) + encryptionKey = models.BinaryField(editable=True, blank=False, null=True) class Meta: unique_together = ('uid', 'collection') def __str__(self): - return self.uid + return '{} {}'.format(self.uid, self.collection.uid) @cached_property def content(self): diff --git a/django_etesync/serializers.py b/django_etesync/serializers.py index 2c12592..7a2d3fd 100644 --- a/django_etesync/serializers.py +++ b/django_etesync/serializers.py @@ -43,44 +43,13 @@ class CollectionEncryptionKeyField(BinaryBase64Field): return None -class CollectionSerializer(serializers.ModelSerializer): - encryptionKey = CollectionEncryptionKeyField() - accessLevel = serializers.SerializerMethodField('get_access_level_from_context') - ctag = serializers.SerializerMethodField('get_ctag') - - class Meta: - model = models.Collection - fields = ('uid', 'version', 'accessLevel', 'encryptionKey', 'ctag') - - def get_access_level_from_context(self, obj): +class CollectionContentField(BinaryBase64Field): + def get_attribute(self, instance): request = self.context.get('request', None) if request is not None: - return obj.members.get(user=request.user).accessLevel + return instance.members.get(user=request.user).encryptionKey return None - def get_ctag(self, obj): - last_revision = models.CollectionItemRevision.objects.filter(item__collection=obj).last() - if last_revision is None: - # FIXME: what is the etag for None? Though if we use the revision for collection it should be shared anyway. - return None - - return last_revision.uid - - def create(self, validated_data): - """Function that's called when this serializer creates an item""" - encryption_key = validated_data.pop('encryptionKey') - instance = self.__class__.Meta.model(**validated_data) - - with transaction.atomic(): - instance.save() - models.CollectionMember(collection=instance, - user=validated_data.get('owner'), - accessLevel=models.CollectionMember.AccessLevels.ADMIN, - encryptionKey=encryption_key, - ).save() - - return instance - class CollectionItemChunkSerializer(serializers.ModelSerializer): class Meta: @@ -177,3 +146,53 @@ class CollectionItemSerializer(serializers.ModelSerializer): class CollectionItemInlineSerializer(CollectionItemSerializer): content = CollectionItemRevisionInlineSerializer(read_only=True, many=False) + + +class CollectionSerializer(serializers.ModelSerializer): + encryptionKey = CollectionEncryptionKeyField() + accessLevel = serializers.SerializerMethodField('get_access_level_from_context') + ctag = serializers.SerializerMethodField('get_ctag') + content = CollectionItemRevisionSerializer(many=False) + + class Meta: + model = models.Collection + fields = ('uid', 'version', 'accessLevel', 'encryptionKey', 'content', 'ctag') + + def get_access_level_from_context(self, obj): + request = self.context.get('request', None) + if request is not None: + return obj.members.get(user=request.user).accessLevel + return None + + def get_ctag(self, obj): + last_revision = models.CollectionItemRevision.objects.filter(item__collection=obj).last() + if last_revision is None: + # FIXME: what is the etag for None? Though if we use the revision for collection it should be shared anyway. + return None + + return last_revision.uid + + def create(self, validated_data): + """Function that's called when this serializer creates an item""" + revision_data = validated_data.pop('content') + encryption_key = validated_data.pop('encryptionKey') + instance = self.__class__.Meta.model(**validated_data) + + with transaction.atomic(): + main_item = models.CollectionItem.objects.create( + uid=None, encryptionKey=None, version=instance.version, collection=instance) + instance.mainItem = main_item + + chunks = revision_data.pop('chunks') + revision = models.CollectionItemRevision.objects.create(**revision_data, uid=generate_rev_uid(), + item=main_item) + revision.chunks.set(chunks) + + instance.save() + models.CollectionMember(collection=instance, + user=validated_data.get('owner'), + accessLevel=models.CollectionMember.AccessLevels.ADMIN, + encryptionKey=encryption_key, + ).save() + + return instance