Change to standalone stoken objects (+ small optimisation).

Makes it possible to now generate Stokens as we need so we can add them to
non-revision objects, for example, membership changes.

We also slightly improved how we filter by revs.
This commit is contained in:
Tom Hacohen 2020-05-26 18:52:44 +03:00
parent 3cdb7783fe
commit 2a39f3538e
3 changed files with 33 additions and 12 deletions

View File

@ -18,6 +18,7 @@ from django.db import models
from django.conf import settings from django.conf import settings
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.crypto import get_random_string
Base64Url256BitlValidator = RegexValidator(regex=r'^[a-zA-Z0-9\-_]{42,43}$', message='Expected a base64url.') Base64Url256BitlValidator = RegexValidator(regex=r'^[a-zA-Z0-9\-_]{42,43}$', message='Expected a base64url.')
@ -55,7 +56,7 @@ class Collection(models.Model):
# FIXME: what is the etag for None? Though if we use the revision for collection it should be shared anyway. # FIXME: what is the etag for None? Though if we use the revision for collection it should be shared anyway.
return None return None
return last_revision.uid return last_revision.stoken.uid
class CollectionItem(models.Model): class CollectionItem(models.Model):
@ -77,7 +78,7 @@ class CollectionItem(models.Model):
@property @property
def stoken(self): def stoken(self):
return self.content.uid return self.content.stoken.uid
def chunk_directory_path(instance, filename): def chunk_directory_path(instance, filename):
@ -98,7 +99,17 @@ class CollectionItemChunk(models.Model):
return self.uid return self.uid
def generate_stoken_uid():
return get_random_string(32, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_')
class Stoken(models.Model):
uid = models.CharField(db_index=True, unique=True, blank=False, null=False, default=generate_stoken_uid,
max_length=43, validators=[Base64Url256BitlValidator])
class CollectionItemRevision(models.Model): class CollectionItemRevision(models.Model):
stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT)
uid = models.CharField(db_index=True, unique=True, blank=False, null=False, uid = models.CharField(db_index=True, unique=True, blank=False, null=False,
max_length=43, validators=[Base64Url256BitlValidator]) max_length=43, validators=[Base64Url256BitlValidator])
item = models.ForeignKey(CollectionItem, related_name='revisions', on_delete=models.CASCADE) item = models.ForeignKey(CollectionItem, related_name='revisions', on_delete=models.CASCADE)

View File

@ -38,7 +38,9 @@ def process_revisions_for_item(item, revision_data):
chunk = models.CollectionItemChunk.objects.get(uid=uid) chunk = models.CollectionItemChunk.objects.get(uid=uid)
chunks_objs.append(chunk) chunks_objs.append(chunk)
revision = models.CollectionItemRevision.objects.create(**revision_data, item=item) stoken = models.Stoken.objects.create()
revision = models.CollectionItemRevision.objects.create(**revision_data, item=item, stoken=stoken)
for chunk in chunks_objs: for chunk in chunks_objs:
models.RevisionChunkRelation.objects.create(chunk=chunk, revision=revision) models.RevisionChunkRelation.objects.create(chunk=chunk, revision=revision)
return revision return revision

View File

@ -35,7 +35,15 @@ import nacl.secret
import nacl.hash import nacl.hash
from . import app_settings, permissions from . import app_settings, permissions
from .models import Collection, CollectionItem, CollectionItemRevision, CollectionMember, CollectionInvitation, UserInfo from .models import (
Collection,
CollectionItem,
CollectionItemRevision,
CollectionMember,
CollectionInvitation,
Stoken,
UserInfo,
)
from .serializers import ( from .serializers import (
b64encode, b64encode,
AuthenticationSignupSerializer, AuthenticationSignupSerializer,
@ -94,18 +102,18 @@ class BaseViewSet(viewsets.ModelViewSet):
user = self.request.user user = self.request.user
return queryset.filter(members__user=user) return queryset.filter(members__user=user)
def get_cstoken_rev(self, request): def get_cstoken_obj(self, request):
cstoken = request.GET.get('cstoken', None) cstoken = request.GET.get('cstoken', None)
if cstoken is not None: if cstoken is not None:
return get_object_or_404(CollectionItemRevision.objects.all(), uid=cstoken) return get_object_or_404(Stoken.objects.all(), uid=cstoken)
return None return None
def filter_by_cstoken(self, request, queryset): def filter_by_cstoken(self, request, queryset):
cstoken_id_field = self.cstoken_id_field + '__id' cstoken_id_field = self.cstoken_id_field + '__id'
cstoken_rev = self.get_cstoken_rev(request) cstoken_rev = self.get_cstoken_obj(request)
if cstoken_rev is not None: if cstoken_rev is not None:
filter_by = {cstoken_id_field + '__gt': cstoken_rev.id} filter_by = {cstoken_id_field + '__gt': cstoken_rev.id}
queryset = queryset.filter(**filter_by) queryset = queryset.filter(**filter_by)
@ -116,7 +124,7 @@ class BaseViewSet(viewsets.ModelViewSet):
cstoken_id_field = self.cstoken_id_field + '__id' cstoken_id_field = self.cstoken_id_field + '__id'
new_cstoken_id = queryset.aggregate(cstoken_id=Max(cstoken_id_field))['cstoken_id'] new_cstoken_id = queryset.aggregate(cstoken_id=Max(cstoken_id_field))['cstoken_id']
new_cstoken = new_cstoken_id and CollectionItemRevision.objects.get(id=new_cstoken_id).uid new_cstoken = new_cstoken_id and Stoken.objects.get(id=new_cstoken_id).uid
return queryset, new_cstoken return queryset, new_cstoken
@ -139,7 +147,7 @@ class CollectionViewSet(BaseViewSet):
queryset = Collection.objects.all() queryset = Collection.objects.all()
serializer_class = CollectionSerializer serializer_class = CollectionSerializer
lookup_field = 'uid' lookup_field = 'uid'
cstoken_id_field = 'items__revisions' cstoken_id_field = 'items__revisions__stoken'
def get_queryset(self, queryset=None): def get_queryset(self, queryset=None):
if queryset is None: if queryset is None:
@ -199,7 +207,7 @@ class CollectionItemViewSet(BaseViewSet):
queryset = CollectionItem.objects.all() queryset = CollectionItem.objects.all()
serializer_class = CollectionItemSerializer serializer_class = CollectionItemSerializer
lookup_field = 'uid' lookup_field = 'uid'
cstoken_id_field = 'revisions' cstoken_id_field = 'revisions__stoken'
def get_queryset(self): def get_queryset(self):
collection_uid = self.kwargs['collection_uid'] collection_uid = self.kwargs['collection_uid']
@ -290,8 +298,8 @@ class CollectionItemViewSet(BaseViewSet):
queryset, cstoken_rev = self.filter_by_cstoken(request, queryset) queryset, cstoken_rev = self.filter_by_cstoken(request, queryset)
uids, stokens = zip(*[(item['uid'], item.get('stoken')) for item in serializer.validated_data]) uids, stokens = zip(*[(item['uid'], item.get('stoken')) for item in serializer.validated_data])
rev_ids = CollectionItemRevision.objects.filter(uid__in=stokens, current=True).values_list('id', flat=True) revs = CollectionItemRevision.objects.filter(stoken__uid__in=stokens, current=True)
queryset = queryset.filter(uid__in=uids).exclude(revisions__id__in=rev_ids) queryset = queryset.filter(uid__in=uids).exclude(revisions__in=revs)
queryset, new_cstoken = self.get_queryset_cstoken(queryset) queryset, new_cstoken = self.get_queryset_cstoken(queryset)
cstoken = cstoken_rev and cstoken_rev.uid cstoken = cstoken_rev and cstoken_rev.uid