mirror of
https://github.com/etesync/server
synced 2024-11-16 05:48:59 +00:00
Collection: change collections to be an extension of items
Each collection now has an item and the item's UID is the collections UID. This lets us manipulate collections just like items, and as part of transactions. This is significant because it lets us change them as part of transactions!
This commit is contained in:
parent
37bae63a46
commit
267d749c45
31
django_etebase/migrations/0016_auto_20200623_0820.py
Normal file
31
django_etebase/migrations/0016_auto_20200623_0820.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 3.0.3 on 2020-06-23 08:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('django_etebase', '0015_collectionitemrevision_salt'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='collection',
|
||||||
|
name='main_item',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parent', to='django_etebase.CollectionItem'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='collection',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='collection',
|
||||||
|
name='uid',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='collection',
|
||||||
|
name='version',
|
||||||
|
),
|
||||||
|
]
|
@ -27,20 +27,15 @@ UidValidator = RegexValidator(regex=r'^[a-zA-Z0-9]*$', message='Not a valid UID'
|
|||||||
|
|
||||||
|
|
||||||
class Collection(models.Model):
|
class Collection(models.Model):
|
||||||
uid = models.CharField(db_index=True, blank=False, null=False,
|
main_item = models.ForeignKey('CollectionItem', related_name='parent', null=True, on_delete=models.SET_NULL)
|
||||||
max_length=43, validators=[UidValidator])
|
|
||||||
version = models.PositiveSmallIntegerField()
|
|
||||||
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = ('uid', 'owner')
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.uid
|
return self.uid
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def main_item(self):
|
def uid(self):
|
||||||
return self.items.get(uid=None)
|
return self.main_item.uid
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def content(self):
|
def content(self):
|
||||||
|
@ -31,7 +31,7 @@ class IsCollectionAdmin(permissions.BasePermission):
|
|||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
collection_uid = view.kwargs['collection_uid']
|
collection_uid = view.kwargs['collection_uid']
|
||||||
try:
|
try:
|
||||||
collection = view.get_collection_queryset().get(uid=collection_uid)
|
collection = view.get_collection_queryset().get(main_item__uid=collection_uid)
|
||||||
return is_collection_admin(collection, request.user)
|
return is_collection_admin(collection, request.user)
|
||||||
except Collection.DoesNotExist:
|
except Collection.DoesNotExist:
|
||||||
# If the collection does not exist, we want to 404 later, not permission denied.
|
# If the collection does not exist, we want to 404 later, not permission denied.
|
||||||
@ -53,7 +53,7 @@ class IsCollectionAdminOrReadOnly(permissions.BasePermission):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
collection = view.get_collection_queryset().get(uid=collection_uid)
|
collection = view.get_collection_queryset().get(main_item__uid=collection_uid)
|
||||||
if request.method in permissions.SAFE_METHODS:
|
if request.method in permissions.SAFE_METHODS:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ class HasWriteAccessOrReadOnly(permissions.BasePermission):
|
|||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
collection_uid = view.kwargs['collection_uid']
|
collection_uid = view.kwargs['collection_uid']
|
||||||
try:
|
try:
|
||||||
collection = view.get_collection_queryset().get(uid=collection_uid)
|
collection = view.get_collection_queryset().get(main_item__uid=collection_uid)
|
||||||
if request.method in permissions.SAFE_METHODS:
|
if request.method in permissions.SAFE_METHODS:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
@ -182,15 +182,19 @@ class CollectionItemBulkGetSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class CollectionSerializer(serializers.ModelSerializer):
|
class CollectionSerializer(serializers.ModelSerializer):
|
||||||
encryptionKey = CollectionEncryptionKeyField()
|
collectionKey = CollectionEncryptionKeyField()
|
||||||
accessLevel = serializers.SerializerMethodField('get_access_level_from_context')
|
accessLevel = serializers.SerializerMethodField('get_access_level_from_context')
|
||||||
stoken = serializers.CharField(read_only=True)
|
stoken = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
uid = serializers.CharField(source='main_item.uid')
|
||||||
|
encryptionKey = BinaryBase64Field(source='main_item.encryptionKey')
|
||||||
etag = serializers.CharField(allow_null=True, write_only=True)
|
etag = serializers.CharField(allow_null=True, write_only=True)
|
||||||
content = CollectionItemRevisionSerializer(many=False)
|
version = serializers.IntegerField(min_value=0, source='main_item.version')
|
||||||
|
content = CollectionItemRevisionSerializer(many=False, source='main_item.content')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Collection
|
model = models.Collection
|
||||||
fields = ('uid', 'version', 'accessLevel', 'encryptionKey', 'content', 'stoken', 'etag')
|
fields = ('uid', 'version', 'accessLevel', 'encryptionKey', 'collectionKey', 'content', 'stoken', 'etag')
|
||||||
|
|
||||||
def get_access_level_from_context(self, obj):
|
def get_access_level_from_context(self, obj):
|
||||||
request = self.context.get('request', None)
|
request = self.context.get('request', None)
|
||||||
@ -200,9 +204,16 @@ class CollectionSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
"""Function that's called when this serializer creates an item"""
|
"""Function that's called when this serializer creates an item"""
|
||||||
|
collection_key = validated_data.pop('collectionKey')
|
||||||
|
|
||||||
etag = validated_data.pop('etag')
|
etag = validated_data.pop('etag')
|
||||||
revision_data = validated_data.pop('content')
|
|
||||||
encryption_key = validated_data.pop('encryptionKey')
|
main_item_data = validated_data.pop('main_item')
|
||||||
|
uid = main_item_data.pop('uid')
|
||||||
|
version = main_item_data.pop('version')
|
||||||
|
revision_data = main_item_data.pop('content')
|
||||||
|
encryption_key = main_item_data.pop('encryptionKey')
|
||||||
|
|
||||||
instance = self.__class__.Meta.model(**validated_data)
|
instance = self.__class__.Meta.model(**validated_data)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
@ -211,7 +222,10 @@ class CollectionSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
instance.save()
|
instance.save()
|
||||||
main_item = models.CollectionItem.objects.create(
|
main_item = models.CollectionItem.objects.create(
|
||||||
uid=None, encryptionKey=None, version=instance.version, collection=instance)
|
uid=uid, encryptionKey=encryption_key, version=version, collection=instance)
|
||||||
|
|
||||||
|
instance.main_item = main_item
|
||||||
|
instance.save()
|
||||||
|
|
||||||
process_revisions_for_item(main_item, revision_data)
|
process_revisions_for_item(main_item, revision_data)
|
||||||
|
|
||||||
@ -219,26 +233,13 @@ class CollectionSerializer(serializers.ModelSerializer):
|
|||||||
stoken=models.Stoken.objects.create(),
|
stoken=models.Stoken.objects.create(),
|
||||||
user=validated_data.get('owner'),
|
user=validated_data.get('owner'),
|
||||||
accessLevel=models.AccessLevels.ADMIN,
|
accessLevel=models.AccessLevels.ADMIN,
|
||||||
encryptionKey=encryption_key,
|
encryptionKey=collection_key,
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
"""Function that's called when this serializer is meant to update an item"""
|
raise NotImplementedError()
|
||||||
revision_data = validated_data.pop('content')
|
|
||||||
|
|
||||||
with transaction.atomic():
|
|
||||||
main_item = instance.main_item
|
|
||||||
# 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 = main_item.revisions.filter(current=True).select_for_update().first()
|
|
||||||
current_revision.current = None
|
|
||||||
current_revision.save()
|
|
||||||
|
|
||||||
process_revisions_for_item(main_item, revision_data)
|
|
||||||
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
class CollectionMemberSerializer(serializers.ModelSerializer):
|
class CollectionMemberSerializer(serializers.ModelSerializer):
|
||||||
|
@ -147,11 +147,12 @@ class BaseViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class CollectionViewSet(BaseViewSet):
|
class CollectionViewSet(BaseViewSet):
|
||||||
allowed_methods = ['GET', 'POST', 'DELETE']
|
allowed_methods = ['GET', 'POST']
|
||||||
permission_classes = BaseViewSet.permission_classes + (permissions.IsCollectionAdminOrReadOnly, )
|
permission_classes = BaseViewSet.permission_classes + (permissions.IsCollectionAdminOrReadOnly, )
|
||||||
queryset = Collection.objects.all()
|
queryset = Collection.objects.all()
|
||||||
serializer_class = CollectionSerializer
|
serializer_class = CollectionSerializer
|
||||||
lookup_field = 'uid'
|
lookup_field = 'main_item__uid'
|
||||||
|
lookup_url_kwarg = 'uid'
|
||||||
stoken_id_fields = ['items__revisions__stoken__id', 'members__stoken__id']
|
stoken_id_fields = ['items__revisions__stoken__id', 'members__stoken__id']
|
||||||
|
|
||||||
def get_queryset(self, queryset=None):
|
def get_queryset(self, queryset=None):
|
||||||
@ -173,19 +174,7 @@ class CollectionViewSet(BaseViewSet):
|
|||||||
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
|
|
||||||
stoken = request.GET.get('stoken', None)
|
|
||||||
|
|
||||||
if stoken is not None and stoken != instance.stoken:
|
|
||||||
content = {'code': 'stale_stoken', 'detail': 'Stoken is too old'}
|
|
||||||
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
serializer = self.get_serializer(instance, data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
self.perform_update(serializer)
|
|
||||||
|
|
||||||
return Response({})
|
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
@ -216,7 +205,7 @@ class CollectionViewSet(BaseViewSet):
|
|||||||
if stoken_obj is not None:
|
if stoken_obj is not None:
|
||||||
# FIXME: honour limit? (the limit should be combined for data and this because of stoken)
|
# FIXME: honour limit? (the limit should be combined for data and this because of stoken)
|
||||||
remed = CollectionMemberRemoved.objects.filter(user=request.user, stoken__id__gt=stoken_obj.id) \
|
remed = CollectionMemberRemoved.objects.filter(user=request.user, stoken__id__gt=stoken_obj.id) \
|
||||||
.values_list('collection__uid', flat=True)
|
.values_list('collection__main_item__uid', flat=True)
|
||||||
if len(remed) > 0:
|
if len(remed) > 0:
|
||||||
ret['removedMemberships'] = [{'uid': x} for x in remed]
|
ret['removedMemberships'] = [{'uid': x} for x in remed]
|
||||||
|
|
||||||
@ -234,7 +223,7 @@ class CollectionItemViewSet(BaseViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
collection_uid = self.kwargs['collection_uid']
|
collection_uid = self.kwargs['collection_uid']
|
||||||
try:
|
try:
|
||||||
collection = self.get_collection_queryset(Collection.objects).get(uid=collection_uid)
|
collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid)
|
||||||
except Collection.DoesNotExist:
|
except Collection.DoesNotExist:
|
||||||
raise Http404("Collection does not exist")
|
raise Http404("Collection does not exist")
|
||||||
# XXX Potentially add this for performance: .prefetch_related('revisions__chunks')
|
# XXX Potentially add this for performance: .prefetch_related('revisions__chunks')
|
||||||
@ -280,7 +269,7 @@ class CollectionItemViewSet(BaseViewSet):
|
|||||||
@action_decorator(detail=True, methods=['GET'])
|
@action_decorator(detail=True, methods=['GET'])
|
||||||
def revision(self, request, collection_uid=None, uid=None):
|
def revision(self, request, collection_uid=None, uid=None):
|
||||||
# FIXME: need pagination support
|
# FIXME: need pagination support
|
||||||
col = get_object_or_404(self.get_collection_queryset(Collection.objects), uid=collection_uid)
|
col = get_object_or_404(self.get_collection_queryset(Collection.objects), main_item__uid=collection_uid)
|
||||||
col_it = get_object_or_404(col.items, uid=uid)
|
col_it = get_object_or_404(col.items, uid=uid)
|
||||||
|
|
||||||
serializer = CollectionItemRevisionSerializer(col_it.revisions.order_by('-id'), many=True)
|
serializer = CollectionItemRevisionSerializer(col_it.revisions.order_by('-id'), many=True)
|
||||||
@ -336,7 +325,7 @@ class CollectionItemViewSet(BaseViewSet):
|
|||||||
with transaction.atomic(): # We need this for locking on the collection object
|
with transaction.atomic(): # We need this for locking on the collection object
|
||||||
collection_object = get_object_or_404(
|
collection_object = get_object_or_404(
|
||||||
self.get_collection_queryset(Collection.objects).select_for_update(), # Lock writes on the collection
|
self.get_collection_queryset(Collection.objects).select_for_update(), # Lock writes on the collection
|
||||||
uid=collection_uid)
|
main_item__uid=collection_uid)
|
||||||
|
|
||||||
if stoken is not None and stoken != collection_object.stoken:
|
if stoken is not None and stoken != collection_object.stoken:
|
||||||
content = {'code': 'stale_stoken', 'detail': 'Stoken is too old'}
|
content = {'code': 'stale_stoken', 'detail': 'Stoken is too old'}
|
||||||
@ -388,7 +377,7 @@ class CollectionItemChunkViewSet(viewsets.ViewSet):
|
|||||||
return queryset.filter(members__user=user)
|
return queryset.filter(members__user=user)
|
||||||
|
|
||||||
def create(self, request, collection_uid=None, collection_item_uid=None):
|
def create(self, request, collection_uid=None, collection_item_uid=None):
|
||||||
col = get_object_or_404(self.get_collection_queryset(), uid=collection_uid)
|
col = get_object_or_404(self.get_collection_queryset(), main_item__uid=collection_uid)
|
||||||
col_it = get_object_or_404(col.items, uid=collection_item_uid)
|
col_it = get_object_or_404(col.items, uid=collection_item_uid)
|
||||||
|
|
||||||
serializer = self.get_serializer_class()(data=request.data)
|
serializer = self.get_serializer_class()(data=request.data)
|
||||||
@ -408,7 +397,7 @@ class CollectionItemChunkViewSet(viewsets.ViewSet):
|
|||||||
import os
|
import os
|
||||||
from django.views.static import serve
|
from django.views.static import serve
|
||||||
|
|
||||||
col = get_object_or_404(self.get_collection_queryset(), uid=collection_uid)
|
col = get_object_or_404(self.get_collection_queryset(), main_item__uid=collection_uid)
|
||||||
col_it = get_object_or_404(col.items, uid=collection_item_uid)
|
col_it = get_object_or_404(col.items, uid=collection_item_uid)
|
||||||
chunk = get_object_or_404(col_it.chunks, uid=uid)
|
chunk = get_object_or_404(col_it.chunks, uid=uid)
|
||||||
|
|
||||||
@ -436,7 +425,7 @@ class CollectionMemberViewSet(BaseViewSet):
|
|||||||
def get_queryset(self, queryset=None):
|
def get_queryset(self, queryset=None):
|
||||||
collection_uid = self.kwargs['collection_uid']
|
collection_uid = self.kwargs['collection_uid']
|
||||||
try:
|
try:
|
||||||
collection = self.get_collection_queryset(Collection.objects).get(uid=collection_uid)
|
collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid)
|
||||||
except Collection.DoesNotExist:
|
except Collection.DoesNotExist:
|
||||||
raise Http404('Collection does not exist')
|
raise Http404('Collection does not exist')
|
||||||
|
|
||||||
@ -478,7 +467,7 @@ class CollectionMemberViewSet(BaseViewSet):
|
|||||||
@action_decorator(detail=False, methods=['POST'], permission_classes=our_base_permission_classes)
|
@action_decorator(detail=False, methods=['POST'], permission_classes=our_base_permission_classes)
|
||||||
def leave(self, request, collection_uid=None):
|
def leave(self, request, collection_uid=None):
|
||||||
collection_uid = self.kwargs['collection_uid']
|
collection_uid = self.kwargs['collection_uid']
|
||||||
col = get_object_or_404(self.get_collection_queryset(Collection.objects), uid=collection_uid)
|
col = get_object_or_404(self.get_collection_queryset(Collection.objects), main_item__uid=collection_uid)
|
||||||
|
|
||||||
member = col.members.get(user=request.user)
|
member = col.members.get(user=request.user)
|
||||||
self.perform_destroy(member)
|
self.perform_destroy(member)
|
||||||
@ -534,7 +523,7 @@ class InvitationOutgoingViewSet(InvitationBaseViewSet):
|
|||||||
collection_uid = serializer.validated_data.get('collection', {}).get('uid')
|
collection_uid = serializer.validated_data.get('collection', {}).get('uid')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
collection = self.get_collection_queryset(Collection.objects).get(uid=collection_uid)
|
collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid)
|
||||||
except Collection.DoesNotExist:
|
except Collection.DoesNotExist:
|
||||||
raise Http404('Collection does not exist')
|
raise Http404('Collection does not exist')
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user