diff --git a/django_etebase/migrations/0033_collection_uid.py b/django_etebase/migrations/0033_collection_uid.py new file mode 100644 index 0000000..e4a829e --- /dev/null +++ b/django_etebase/migrations/0033_collection_uid.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.1 on 2020-12-14 11:21 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_etebase', '0032_auto_20201013_1409'), + ] + + operations = [ + migrations.AddField( + model_name='collection', + name='uid', + field=models.CharField(db_index=True, max_length=43, null=True, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]{20,}$')]), + ), + ] diff --git a/django_etebase/migrations/0034_auto_20201214_1124.py b/django_etebase/migrations/0034_auto_20201214_1124.py new file mode 100644 index 0000000..b05060b --- /dev/null +++ b/django_etebase/migrations/0034_auto_20201214_1124.py @@ -0,0 +1,22 @@ +# Generated by Django 3.1.1 on 2020-12-14 11:24 + +from django.db import migrations + + +def update_collection_uid(apps, schema_editor): + Collection = apps.get_model("django_etebase", "Collection") + + for collection in Collection.objects.all(): + collection.uid = collection.main_item.uid + collection.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("django_etebase", "0033_collection_uid"), + ] + + operations = [ + migrations.RunPython(update_collection_uid), + ] diff --git a/django_etebase/migrations/0035_auto_20201214_1126.py b/django_etebase/migrations/0035_auto_20201214_1126.py new file mode 100644 index 0000000..489c5e0 --- /dev/null +++ b/django_etebase/migrations/0035_auto_20201214_1126.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.1 on 2020-12-14 11:26 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_etebase', '0034_auto_20201214_1124'), + ] + + operations = [ + migrations.AlterField( + model_name='collection', + name='uid', + field=models.CharField(db_index=True, max_length=43, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]{20,}$')]), + ), + ] diff --git a/django_etebase/migrations/0036_auto_20201214_1128.py b/django_etebase/migrations/0036_auto_20201214_1128.py new file mode 100644 index 0000000..241adf1 --- /dev/null +++ b/django_etebase/migrations/0036_auto_20201214_1128.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.1 on 2020-12-14 11:28 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_etebase', '0035_auto_20201214_1126'), + ] + + operations = [ + migrations.AlterField( + model_name='collection', + name='uid', + field=models.CharField(db_index=True, max_length=43, unique=True, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]{20,}$')]), + ), + ] diff --git a/django_etebase/models.py b/django_etebase/models.py index af4d022..11b6506 100644 --- a/django_etebase/models.py +++ b/django_etebase/models.py @@ -43,6 +43,8 @@ class CollectionType(models.Model): class Collection(models.Model): main_item = models.OneToOneField("CollectionItem", related_name="parent", null=True, on_delete=models.SET_NULL) + # The same as main_item.uid, we just also save it here so we have DB constraints for uniqueness (and efficiency) + uid = models.CharField(db_index=True, unique=True, blank=False, max_length=43, validators=[UidValidator]) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) stoken_annotation = stoken_annotation_builder(["items__revisions__stoken", "members__stoken"]) @@ -50,10 +52,6 @@ class Collection(models.Model): def __str__(self): return self.uid - @cached_property - def uid(self): - return self.main_item.uid - @property def content(self): return self.main_item.content @@ -76,20 +74,6 @@ class Collection(models.Model): return Stoken.objects.get(id=stoken_id).uid - def validate_unique(self, exclude=None): - super().validate_unique(exclude=exclude) - if exclude is None or "main_item" in exclude: - return - - if ( - self.__class__.objects.filter(owner=self.owner, main_item__uid=self.main_item.uid) - .exclude(id=self.id) - .exists() - ): - raise EtebaseValidationError( - "unique_uid", "Collection with this uid already exists", status_code=status.HTTP_409_CONFLICT - ) - class CollectionItem(models.Model): uid = models.CharField(db_index=True, blank=False, max_length=43, validators=[UidValidator]) diff --git a/django_etebase/serializers.py b/django_etebase/serializers.py index b0769ef..9628986 100644 --- a/django_etebase/serializers.py +++ b/django_etebase/serializers.py @@ -313,13 +313,13 @@ class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer): user = validated_data.get("owner") main_item_data = validated_data.pop("main_item") + uid = main_item_data.get("uid") etag = main_item_data.pop("etag") revision_data = main_item_data.pop("content") - instance = self.__class__.Meta.model(**validated_data) + instance = self.__class__.Meta.model(uid=uid, **validated_data) with transaction.atomic(): - _ = self.__class__.Meta.model.objects.select_for_update().filter(owner=user) if etag is not None: raise EtebaseValidationError("bad_etag", "etag is not null") diff --git a/django_etebase/views.py b/django_etebase/views.py index d1266c6..44c3383 100644 --- a/django_etebase/views.py +++ b/django_etebase/views.py @@ -174,7 +174,7 @@ class CollectionViewSet(BaseViewSet): permission_classes = BaseViewSet.permission_classes + (permissions.IsCollectionAdminOrReadOnly,) queryset = Collection.objects.all() serializer_class = CollectionSerializer - lookup_field = "main_item__uid" + lookup_field = "uid" lookup_url_kwarg = "uid" stoken_annotation = Collection.stoken_annotation @@ -246,7 +246,7 @@ class CollectionViewSet(BaseViewSet): # can point to the most recent collection change rather than most recent removed membership. remed_qs = remed_qs.filter(stoken__id__lte=new_stoken_obj.id) - remed = remed_qs.values_list("collection__main_item__uid", flat=True) + remed = remed_qs.values_list("collection__uid", flat=True) if len(remed) > 0: ret["removedMemberships"] = [{"uid": x} for x in remed] @@ -264,7 +264,7 @@ class CollectionItemViewSet(BaseViewSet): def get_queryset(self): collection_uid = self.kwargs["collection_uid"] try: - collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid) + collection = self.get_collection_queryset(Collection.objects).get(uid=collection_uid) except Collection.DoesNotExist: raise Http404("Collection does not exist") # XXX Potentially add this for performance: .prefetch_related('revisions__chunks') @@ -312,7 +312,7 @@ class CollectionItemViewSet(BaseViewSet): @action_decorator(detail=True, methods=["GET"]) def revision(self, request, collection_uid=None, uid=None, *args, **kwargs): - col = get_object_or_404(self.get_collection_queryset(Collection.objects), main_item__uid=collection_uid) + col = get_object_or_404(self.get_collection_queryset(Collection.objects), uid=collection_uid) item = get_object_or_404(col.items, uid=uid) limit = int(request.GET.get("limit", 50)) @@ -386,7 +386,7 @@ class CollectionItemViewSet(BaseViewSet): with transaction.atomic(): # We need this for locking on the collection object collection_object = get_object_or_404( self.get_collection_queryset(Collection.objects).select_for_update(), # Lock writes on the collection - main_item__uid=collection_uid, + uid=collection_uid, ) if stoken is not None and stoken != collection_object.stoken: @@ -435,7 +435,7 @@ class CollectionItemChunkViewSet(viewsets.ViewSet): return queryset.filter(members__user=user) def update(self, request, *args, collection_uid=None, collection_item_uid=None, uid=None, **kwargs): - col = get_object_or_404(self.get_collection_queryset(), main_item__uid=collection_uid) + col = get_object_or_404(self.get_collection_queryset(), uid=collection_uid) # IGNORED FOR NOW: col_it = get_object_or_404(col.items, uid=collection_item_uid) data = { @@ -459,7 +459,7 @@ class CollectionItemChunkViewSet(viewsets.ViewSet): import os from django.views.static import serve - col = get_object_or_404(self.get_collection_queryset(), main_item__uid=collection_uid) + col = get_object_or_404(self.get_collection_queryset(), uid=collection_uid) # IGNORED FOR NOW: col_it = get_object_or_404(col.items, uid=collection_item_uid) chunk = get_object_or_404(col.chunks, uid=uid) @@ -487,7 +487,7 @@ class CollectionMemberViewSet(BaseViewSet): def get_queryset(self, queryset=None): collection_uid = self.kwargs["collection_uid"] try: - collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid) + collection = self.get_collection_queryset(Collection.objects).get(uid=collection_uid) except Collection.DoesNotExist: raise Http404("Collection does not exist") @@ -525,7 +525,7 @@ class CollectionMemberViewSet(BaseViewSet): @action_decorator(detail=False, methods=["POST"], permission_classes=our_base_permission_classes) def leave(self, request, collection_uid=None, *args, **kwargs): collection_uid = self.kwargs["collection_uid"] - col = get_object_or_404(self.get_collection_queryset(Collection.objects), main_item__uid=collection_uid) + col = get_object_or_404(self.get_collection_queryset(Collection.objects), uid=collection_uid) member = col.members.get(user=request.user) self.perform_destroy(member) @@ -584,7 +584,7 @@ class InvitationOutgoingViewSet(InvitationBaseViewSet): collection_uid = serializer.validated_data.get("collection", {}).get("uid") try: - collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid) + collection = self.get_collection_queryset(Collection.objects).get(uid=collection_uid) except Collection.DoesNotExist: raise Http404("Collection does not exist")