From 5d8a92f0001c6595826c2def2af25d509c69ef52 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 13 Oct 2020 17:13:07 +0300 Subject: [PATCH] Collections: add support for collection types. We also added the field for invitations, as it's needed for collections to work. --- .../migrations/0031_auto_20201013_1336.py | 29 ++++++++++++++++ .../migrations/0032_auto_20201013_1409.py | 18 ++++++++++ django_etebase/models.py | 6 ++++ django_etebase/serializers.py | 34 +++++++++++++++++-- django_etebase/views.py | 16 +++++++++ 5 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 django_etebase/migrations/0031_auto_20201013_1336.py create mode 100644 django_etebase/migrations/0032_auto_20201013_1409.py diff --git a/django_etebase/migrations/0031_auto_20201013_1336.py b/django_etebase/migrations/0031_auto_20201013_1336.py new file mode 100644 index 0000000..ca45dd4 --- /dev/null +++ b/django_etebase/migrations/0031_auto_20201013_1336.py @@ -0,0 +1,29 @@ +# Generated by Django 3.1.1 on 2020-10-13 13:36 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('django_etebase', '0030_auto_20200922_0832'), + ] + + operations = [ + migrations.CreateModel( + name='CollectionType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uid', models.BinaryField(db_index=True, editable=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='collectionmember', + name='collectionType', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etebase.collectiontype'), + ), + ] diff --git a/django_etebase/migrations/0032_auto_20201013_1409.py b/django_etebase/migrations/0032_auto_20201013_1409.py new file mode 100644 index 0000000..5594006 --- /dev/null +++ b/django_etebase/migrations/0032_auto_20201013_1409.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.1 on 2020-10-13 14:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_etebase', '0031_auto_20201013_1336'), + ] + + operations = [ + migrations.AlterField( + model_name='collectiontype', + name='uid', + field=models.BinaryField(db_index=True, editable=True, unique=True), + ), + ] diff --git a/django_etebase/models.py b/django_etebase/models.py index 914b763..0036884 100644 --- a/django_etebase/models.py +++ b/django_etebase/models.py @@ -30,6 +30,11 @@ from .exceptions import EtebaseValidationError UidValidator = RegexValidator(regex=r'^[a-zA-Z0-9\-_]{20,}$', message='Not a valid UID') +class CollectionType(models.Model): + owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + uid = models.BinaryField(editable=True, blank=False, null=False, db_index=True, unique=True) + + class Collection(models.Model): main_item = models.OneToOneField('CollectionItem', related_name='parent', null=True, on_delete=models.SET_NULL) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) @@ -162,6 +167,7 @@ class CollectionMember(models.Model): collection = models.ForeignKey(Collection, related_name='members', on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) encryptionKey = models.BinaryField(editable=True, blank=False, null=False) + collectionType = models.ForeignKey(CollectionType, on_delete=models.PROTECT, null=True) accessLevel = models.IntegerField( choices=AccessLevels.choices, default=AccessLevels.READ_ONLY, diff --git a/django_etebase/serializers.py b/django_etebase/serializers.py index fe9ae11..58ad10c 100644 --- a/django_etebase/serializers.py +++ b/django_etebase/serializers.py @@ -86,6 +86,15 @@ class CollectionEncryptionKeyField(BinaryBase64Field): return None +class CollectionTypeField(BinaryBase64Field): + def get_attribute(self, instance): + request = self.context.get('request', None) + if request is not None: + collection_type = instance.members.get(user=request.user).collectionType + return collection_type and collection_type.uid + return None + + class UserSlugRelatedField(serializers.SlugRelatedField): def get_queryset(self): view = self.context.get('view', None) @@ -256,8 +265,15 @@ class CollectionItemBulkGetSerializer(BetterErrorsMixin, serializers.ModelSerial fields = ('uid', 'etag') +class CollectionListMultiSerializer(BetterErrorsMixin, serializers.Serializer): + collectionTypes = serializers.ListField( + child=BinaryBase64Field() + ) + + class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer): collectionKey = CollectionEncryptionKeyField() + collectionType = CollectionTypeField() accessLevel = serializers.SerializerMethodField('get_access_level_from_context') stoken = serializers.CharField(read_only=True) @@ -265,7 +281,7 @@ class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer): class Meta: model = models.Collection - fields = ('item', 'accessLevel', 'collectionKey', 'stoken') + fields = ('item', 'accessLevel', 'collectionKey', 'collectionType', 'stoken') def get_access_level_from_context(self, obj): request = self.context.get('request', None) @@ -276,6 +292,7 @@ class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer): def create(self, validated_data): """Function that's called when this serializer creates an item""" collection_key = validated_data.pop('collectionKey') + collection_type = validated_data.pop('collectionType') main_item_data = validated_data.pop('main_item') etag = main_item_data.pop('etag') @@ -297,11 +314,16 @@ class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer): process_revisions_for_item(main_item, revision_data) + user = validated_data.get('owner') + + collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=collection_type, owner=user) + models.CollectionMember(collection=instance, stoken=models.Stoken.objects.create(), - user=validated_data.get('owner'), + user=user, accessLevel=models.AccessLevels.ADMIN, encryptionKey=collection_key, + collectionType=collection_type_obj, ).save() return instance @@ -381,6 +403,7 @@ class CollectionInvitationSerializer(BetterErrorsMixin, serializers.ModelSeriali class InvitationAcceptSerializer(BetterErrorsMixin, serializers.Serializer): + collectionType = BinaryBase64Field() encryptionKey = BinaryBase64Field() def create(self, validated_data): @@ -388,13 +411,18 @@ class InvitationAcceptSerializer(BetterErrorsMixin, serializers.Serializer): with transaction.atomic(): invitation = self.context['invitation'] encryption_key = validated_data.get('encryptionKey') + collection_type = validated_data.pop('collectionType') + + user = invitation.user + collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=collection_type, owner=user) member = models.CollectionMember.objects.create( collection=invitation.collection, stoken=models.Stoken.objects.create(), - user=invitation.user, + user=user, accessLevel=invitation.accessLevel, encryptionKey=encryption_key, + collectionType=collection_type_obj, ) models.CollectionMemberRemoved.objects.filter( diff --git a/django_etebase/views.py b/django_etebase/views.py index c7162dd..34f6254 100644 --- a/django_etebase/views.py +++ b/django_etebase/views.py @@ -66,6 +66,7 @@ from .serializers import ( CollectionItemDepSerializer, CollectionItemRevisionSerializer, CollectionItemChunkSerializer, + CollectionListMultiSerializer, CollectionMemberSerializer, CollectionInvitationSerializer, InvitationAcceptSerializer, @@ -210,6 +211,21 @@ class CollectionViewSet(BaseViewSet): def list(self, request, *args, **kwargs): queryset = self.get_queryset() + return self.list_common(request, queryset, *args, **kwargs) + + @action_decorator(detail=False, methods=['POST']) + def list_multi(self, request, *args, **kwargs): + serializer = CollectionListMultiSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + collection_types = serializer.validated_data['collectionTypes'] + + queryset = self.get_queryset() + queryset = queryset.filter(members__collectionType__uid__in=collection_types) + + return self.list_common(request, queryset, *args, **kwargs) + + def list_common(self, request, queryset, *args, **kwargs): result, new_stoken_obj, done = self.filter_by_stoken_and_limit(request, queryset) new_stoken = new_stoken_obj and new_stoken_obj.uid