Use black for code formatting and format the code

Merge #65
This commit is contained in:
Tal Leibman 2020-11-14 17:04:41 +02:00 committed by GitHub
parent 9ec16e9216
commit d8e5c37db1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 1094 additions and 832 deletions

View File

@ -21,18 +21,19 @@ class AppSettings:
def import_from_str(self, name): def import_from_str(self, name):
from importlib import import_module from importlib import import_module
path, prop = name.rsplit('.', 1) path, prop = name.rsplit(".", 1)
mod = import_module(path) mod = import_module(path)
return getattr(mod, prop) return getattr(mod, prop)
def _setting(self, name, dflt): def _setting(self, name, dflt):
from django.conf import settings from django.conf import settings
return getattr(settings, self.prefix + name, dflt) return getattr(settings, self.prefix + name, dflt)
@cached_property @cached_property
def API_PERMISSIONS(self): # pylint: disable=invalid-name def API_PERMISSIONS(self): # pylint: disable=invalid-name
perms = self._setting("API_PERMISSIONS", ('rest_framework.permissions.IsAuthenticated', )) perms = self._setting("API_PERMISSIONS", ("rest_framework.permissions.IsAuthenticated",))
ret = [] ret = []
for perm in perms: for perm in perms:
ret.append(self.import_from_str(perm)) ret.append(self.import_from_str(perm))
@ -40,8 +41,13 @@ class AppSettings:
@cached_property @cached_property
def API_AUTHENTICATORS(self): # pylint: disable=invalid-name def API_AUTHENTICATORS(self): # pylint: disable=invalid-name
perms = self._setting("API_AUTHENTICATORS", ('rest_framework.authentication.TokenAuthentication', perms = self._setting(
'rest_framework.authentication.SessionAuthentication')) "API_AUTHENTICATORS",
(
"rest_framework.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
)
ret = [] ret = []
for perm in perms: for perm in perms:
ret.append(self.import_from_str(perm)) ret.append(self.import_from_str(perm))
@ -80,4 +86,4 @@ class AppSettings:
return self._setting("CHALLENGE_VALID_SECONDS", 60) return self._setting("CHALLENGE_VALID_SECONDS", 60)
app_settings = AppSettings('ETEBASE_') app_settings = AppSettings("ETEBASE_")

View File

@ -2,4 +2,4 @@ from django.apps import AppConfig
class DjangoEtebaseConfig(AppConfig): class DjangoEtebaseConfig(AppConfig):
name = 'django_etebase' name = "django_etebase"

View File

@ -2,4 +2,4 @@ from django.apps import AppConfig
class DrfMsgpackConfig(AppConfig): class DrfMsgpackConfig(AppConfig):
name = 'drf_msgpack' name = "drf_msgpack"

View File

@ -5,10 +5,10 @@ from rest_framework.exceptions import ParseError
class MessagePackParser(BaseParser): class MessagePackParser(BaseParser):
media_type = 'application/msgpack' media_type = "application/msgpack"
def parse(self, stream, media_type=None, parser_context=None): def parse(self, stream, media_type=None, parser_context=None):
try: try:
return msgpack.unpackb(stream.read(), raw=False) return msgpack.unpackb(stream.read(), raw=False)
except Exception as exc: except Exception as exc:
raise ParseError('MessagePack parse error - %s' % str(exc)) raise ParseError("MessagePack parse error - %s" % str(exc))

View File

@ -4,12 +4,12 @@ from rest_framework.renderers import BaseRenderer
class MessagePackRenderer(BaseRenderer): class MessagePackRenderer(BaseRenderer):
media_type = 'application/msgpack' media_type = "application/msgpack"
format = 'msgpack' format = "msgpack"
render_style = 'binary' render_style = "binary"
charset = None charset = None
def render(self, data, media_type=None, renderer_context=None): def render(self, data, media_type=None, renderer_context=None):
if data is None: if data is None:
return b'' return b""
return msgpack.packb(data, use_bin_type=True) return msgpack.packb(data, use_bin_type=True)

View File

@ -3,8 +3,7 @@ from rest_framework import serializers, status
class EtebaseValidationError(serializers.ValidationError): class EtebaseValidationError(serializers.ValidationError):
def __init__(self, code, detail, status_code=status.HTTP_400_BAD_REQUEST): def __init__(self, code, detail, status_code=status.HTTP_400_BAD_REQUEST):
super().__init__({ super().__init__(
'code': code, {"code": code, "detail": detail,}
'detail': detail, )
})
self.status_code = status_code self.status_code = status_code

View File

@ -17,75 +17,159 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Collection', name="Collection",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('uid', models.CharField(db_index=True, max_length=44, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='[a-zA-Z0-9]')])), (
('version', models.PositiveSmallIntegerField()), "uid",
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), models.CharField(
db_index=True,
max_length=44,
validators=[
django.core.validators.RegexValidator(message="Not a valid UID", regex="[a-zA-Z0-9]")
], ],
options={ ),
'unique_together': {('uid', 'owner')}, ),
}, ("version", models.PositiveSmallIntegerField()),
("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={"unique_together": {("uid", "owner")},},
), ),
migrations.CreateModel( migrations.CreateModel(
name='CollectionItem', name="CollectionItem",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('uid', 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]')])), (
('version', models.PositiveSmallIntegerField()), "uid",
('encryptionKey', models.BinaryField(editable=True, null=True)), models.CharField(
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='django_etebase.Collection')), db_index=True,
max_length=44,
null=True,
validators=[
django.core.validators.RegexValidator(message="Not a valid UID", regex="[a-zA-Z0-9]")
], ],
options={ ),
'unique_together': {('uid', 'collection')}, ),
}, ("version", models.PositiveSmallIntegerField()),
("encryptionKey", models.BinaryField(editable=True, null=True)),
(
"collection",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="items",
to="django_etebase.Collection",
),
),
],
options={"unique_together": {("uid", "collection")},},
), ),
migrations.CreateModel( migrations.CreateModel(
name='CollectionItemChunk', name="CollectionItemChunk",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('uid', models.CharField(db_index=True, max_length=44, validators=[django.core.validators.RegexValidator(message='Expected a 256bit base64url.', regex='^[a-zA-Z0-9\\-_]{43}$')])), (
('chunkFile', models.FileField(max_length=150, unique=True, upload_to=django_etebase.models.chunk_directory_path)), "uid",
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='django_etebase.CollectionItem')), models.CharField(
db_index=True,
max_length=44,
validators=[
django.core.validators.RegexValidator(
message="Expected a 256bit base64url.", regex="^[a-zA-Z0-9\\-_]{43}$"
)
],
),
),
(
"chunkFile",
models.FileField(max_length=150, unique=True, upload_to=django_etebase.models.chunk_directory_path),
),
(
"item",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="chunks",
to="django_etebase.CollectionItem",
),
),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='CollectionItemRevision', name="CollectionItemRevision",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('uid', models.CharField(db_index=True, max_length=44, unique=True, validators=[django.core.validators.RegexValidator(message='Expected a 256bit base64url.', regex='^[a-zA-Z0-9\\-_]{43}$')])), (
('meta', models.BinaryField(editable=True)), "uid",
('current', models.BooleanField(db_index=True, default=True, null=True)), models.CharField(
('deleted', models.BooleanField(default=False)), db_index=True,
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='django_etebase.CollectionItem')), max_length=44,
unique=True,
validators=[
django.core.validators.RegexValidator(
message="Expected a 256bit base64url.", regex="^[a-zA-Z0-9\\-_]{43}$"
)
], ],
options={ ),
'unique_together': {('item', 'current')}, ),
}, ("meta", models.BinaryField(editable=True)),
("current", models.BooleanField(db_index=True, default=True, null=True)),
("deleted", models.BooleanField(default=False)),
(
"item",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="revisions",
to="django_etebase.CollectionItem",
),
),
],
options={"unique_together": {("item", "current")},},
), ),
migrations.CreateModel( migrations.CreateModel(
name='RevisionChunkRelation', name="RevisionChunkRelation",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('chunk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions_relation', to='django_etebase.CollectionItemChunk')), (
('revision', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chunks_relation', to='django_etebase.CollectionItemRevision')), "chunk",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="revisions_relation",
to="django_etebase.CollectionItemChunk",
),
),
(
"revision",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="chunks_relation",
to="django_etebase.CollectionItemRevision",
),
),
], ],
options={ options={"ordering": ("id",),},
'ordering': ('id',),
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='CollectionMember', name="CollectionMember",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('encryptionKey', models.BinaryField(editable=True)), ("encryptionKey", models.BinaryField(editable=True)),
('accessLevel', models.CharField(choices=[('adm', 'Admin'), ('rw', 'Read Write'), ('ro', 'Read Only')], default='ro', max_length=3)), (
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='members', to='django_etebase.Collection')), "accessLevel",
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), models.CharField(
choices=[("adm", "Admin"), ("rw", "Read Write"), ("ro", "Read Only")],
default="ro",
max_length=3,
),
),
(
"collection",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="members",
to="django_etebase.Collection",
),
),
("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
options={ options={"unique_together": {("user", "collection")},},
'unique_together': {('user', 'collection')},
},
), ),
] ]

View File

@ -8,18 +8,26 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('myauth', '0001_initial'), ("myauth", "0001_initial"),
('django_etebase', '0001_initial'), ("django_etebase", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='UserInfo', name="UserInfo",
fields=[ fields=[
('owner', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), (
('version', models.PositiveSmallIntegerField(default=1)), "owner",
('pubkey', models.BinaryField(editable=True)), models.OneToOneField(
('salt', models.BinaryField(editable=True)), on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to=settings.AUTH_USER_MODEL,
),
),
("version", models.PositiveSmallIntegerField(default=1)),
("pubkey", models.BinaryField(editable=True)),
("salt", models.BinaryField(editable=True)),
], ],
), ),
] ]

View File

@ -10,22 +10,50 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('django_etebase', '0002_userinfo'), ("django_etebase", "0002_userinfo"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='CollectionInvitation', name="CollectionInvitation",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('uid', models.CharField(db_index=True, max_length=44, validators=[django.core.validators.RegexValidator(message='Expected a 256bit base64url.', regex='^[a-zA-Z0-9\\-_]{43}$')])), (
('signedEncryptionKey', models.BinaryField()), "uid",
('accessLevel', models.CharField(choices=[('adm', 'Admin'), ('rw', 'Read Write'), ('ro', 'Read Only')], default='ro', max_length=3)), models.CharField(
('fromMember', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_etebase.CollectionMember')), db_index=True,
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='incoming_invitations', to=settings.AUTH_USER_MODEL)), max_length=44,
validators=[
django.core.validators.RegexValidator(
message="Expected a 256bit base64url.", regex="^[a-zA-Z0-9\\-_]{43}$"
)
], ],
options={ ),
'unique_together': {('user', 'fromMember')}, ),
}, ("signedEncryptionKey", models.BinaryField()),
(
"accessLevel",
models.CharField(
choices=[("adm", "Admin"), ("rw", "Read Write"), ("ro", "Read Only")],
default="ro",
max_length=3,
),
),
(
"fromMember",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="django_etebase.CollectionMember"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="incoming_invitations",
to=settings.AUTH_USER_MODEL,
),
),
],
options={"unique_together": {("user", "fromMember")},},
), ),
] ]

View File

@ -6,13 +6,11 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0003_collectioninvitation'), ("django_etebase", "0003_collectioninvitation"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='collectioninvitation', model_name="collectioninvitation", name="version", field=models.PositiveSmallIntegerField(default=1),
name='version',
field=models.PositiveSmallIntegerField(default=1),
), ),
] ]

View File

@ -6,13 +6,9 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0004_collectioninvitation_version'), ("django_etebase", "0004_collectioninvitation_version"),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(model_name="userinfo", old_name="pubkey", new_name="loginPubkey",),
model_name='userinfo',
old_name='pubkey',
new_name='loginPubkey',
),
] ]

View File

@ -6,20 +6,20 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0005_auto_20200526_1021'), ("django_etebase", "0005_auto_20200526_1021"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='userinfo', model_name="userinfo",
name='encryptedSeckey', name="encryptedSeckey",
field=models.BinaryField(default=b'', editable=True), field=models.BinaryField(default=b"", editable=True),
preserve_default=False, preserve_default=False,
), ),
migrations.AddField( migrations.AddField(
model_name='userinfo', model_name="userinfo",
name='pubkey', name="pubkey",
field=models.BinaryField(default=b'', editable=True), field=models.BinaryField(default=b"", editable=True),
preserve_default=False, preserve_default=False,
), ),
] ]

View File

@ -7,33 +7,67 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0006_auto_20200526_1040'), ("django_etebase", "0006_auto_20200526_1040"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='collection', model_name="collection",
name='uid', 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]*$')]), field=models.CharField(
db_index=True,
max_length=43,
validators=[django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9]*$")],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='collectioninvitation', model_name="collectioninvitation",
name='uid', name="uid",
field=models.CharField(db_index=True, max_length=43, validators=[django.core.validators.RegexValidator(message='Expected a base64url.', regex='^[a-zA-Z0-9\\-_]{42,43}$')]), field=models.CharField(
db_index=True,
max_length=43,
validators=[
django.core.validators.RegexValidator(
message="Expected a base64url.", regex="^[a-zA-Z0-9\\-_]{42,43}$"
)
],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='collectionitem', model_name="collectionitem",
name='uid', 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]*$')]), 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]*$")],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='collectionitemchunk', model_name="collectionitemchunk",
name='uid', name="uid",
field=models.CharField(db_index=True, max_length=43, validators=[django.core.validators.RegexValidator(message='Expected a base64url.', regex='^[a-zA-Z0-9\\-_]{42,43}$')]), field=models.CharField(
db_index=True,
max_length=43,
validators=[
django.core.validators.RegexValidator(
message="Expected a base64url.", regex="^[a-zA-Z0-9\\-_]{42,43}$"
)
],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='collectionitemrevision', model_name="collectionitemrevision",
name='uid', name="uid",
field=models.CharField(db_index=True, max_length=43, unique=True, validators=[django.core.validators.RegexValidator(message='Expected a base64url.', regex='^[a-zA-Z0-9\\-_]{42,43}$')]), field=models.CharField(
db_index=True,
max_length=43,
unique=True,
validators=[
django.core.validators.RegexValidator(
message="Expected a base64url.", regex="^[a-zA-Z0-9\\-_]{42,43}$"
)
],
),
), ),
] ]

View File

@ -9,20 +9,35 @@ import django_etebase.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0007_auto_20200526_1336'), ("django_etebase", "0007_auto_20200526_1336"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Stoken', name="Stoken",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('uid', models.CharField(db_index=True, default=django_etebase.models.generate_stoken_uid, max_length=43, unique=True, validators=[django.core.validators.RegexValidator(message='Expected a base64url.', regex='^[a-zA-Z0-9\\-_]{42,43}$')])), (
"uid",
models.CharField(
db_index=True,
default=django_etebase.models.generate_stoken_uid,
max_length=43,
unique=True,
validators=[
django.core.validators.RegexValidator(
message="Expected a base64url.", regex="^[a-zA-Z0-9\\-_]{42,43}$"
)
],
),
),
], ],
), ),
migrations.AddField( migrations.AddField(
model_name='collectionitemrevision', model_name="collectionitemrevision",
name='stoken', name="stoken",
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etebase.Stoken'), field=models.OneToOneField(
null=True, on_delete=django.db.models.deletion.PROTECT, to="django_etebase.Stoken"
),
), ),
] ]

View File

@ -4,8 +4,8 @@ from django.db import migrations
def create_stokens(apps, schema_editor): def create_stokens(apps, schema_editor):
Stoken = apps.get_model('django_etebase', 'Stoken') Stoken = apps.get_model("django_etebase", "Stoken")
CollectionItemRevision = apps.get_model('django_etebase', 'CollectionItemRevision') CollectionItemRevision = apps.get_model("django_etebase", "CollectionItemRevision")
for rev in CollectionItemRevision.objects.all(): for rev in CollectionItemRevision.objects.all():
rev.stoken = Stoken.objects.create() rev.stoken = Stoken.objects.create()
@ -15,7 +15,7 @@ def create_stokens(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0008_auto_20200526_1535'), ("django_etebase", "0008_auto_20200526_1535"),
] ]
operations = [ operations = [

View File

@ -7,13 +7,13 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0009_auto_20200526_1535'), ("django_etebase", "0009_auto_20200526_1535"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='collectionitemrevision', model_name="collectionitemrevision",
name='stoken', name="stoken",
field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='django_etebase.Stoken'), field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to="django_etebase.Stoken"),
), ),
] ]

View File

@ -7,13 +7,15 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0010_auto_20200526_1539'), ("django_etebase", "0010_auto_20200526_1539"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='collectionmember', model_name="collectionmember",
name='stoken', name="stoken",
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etebase.Stoken'), field=models.OneToOneField(
null=True, on_delete=django.db.models.deletion.PROTECT, to="django_etebase.Stoken"
),
), ),
] ]

View File

@ -4,8 +4,8 @@ from django.db import migrations
def create_stokens(apps, schema_editor): def create_stokens(apps, schema_editor):
Stoken = apps.get_model('django_etebase', 'Stoken') Stoken = apps.get_model("django_etebase", "Stoken")
CollectionMember = apps.get_model('django_etebase', 'CollectionMember') CollectionMember = apps.get_model("django_etebase", "CollectionMember")
for member in CollectionMember.objects.all(): for member in CollectionMember.objects.all():
member.stoken = Stoken.objects.create() member.stoken = Stoken.objects.create()
@ -15,7 +15,7 @@ def create_stokens(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0011_collectionmember_stoken'), ("django_etebase", "0011_collectionmember_stoken"),
] ]
operations = [ operations = [

View File

@ -9,20 +9,30 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('django_etebase', '0012_auto_20200527_0743'), ("django_etebase", "0012_auto_20200527_0743"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='CollectionMemberRemoved', name="CollectionMemberRemoved",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='removed_members', to='django_etebase.Collection')), (
('stoken', models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etebase.Stoken')), "collection",
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="removed_members",
to="django_etebase.Collection",
),
),
(
"stoken",
models.OneToOneField(
null=True, on_delete=django.db.models.deletion.PROTECT, to="django_etebase.Stoken"
),
),
("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
options={ options={"unique_together": {("user", "collection")},},
'unique_together': {('user', 'collection')},
},
), ),
] ]

View File

@ -6,13 +6,9 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0013_collectionmemberremoved'), ("django_etebase", "0013_collectionmemberremoved"),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(model_name="userinfo", old_name="encryptedSeckey", new_name="encryptedContent",),
model_name='userinfo',
old_name='encryptedSeckey',
new_name='encryptedContent',
),
] ]

View File

@ -6,13 +6,11 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0014_auto_20200602_1558'), ("django_etebase", "0014_auto_20200602_1558"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='collectionitemrevision', model_name="collectionitemrevision", name="salt", field=models.BinaryField(default=b"", editable=True),
name='salt',
field=models.BinaryField(default=b'', editable=True),
), ),
] ]

View File

@ -7,25 +7,21 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0015_collectionitemrevision_salt'), ("django_etebase", "0015_collectionitemrevision_salt"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='collection', model_name="collection",
name='main_item', name="main_item",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parent', to='django_etebase.CollectionItem'), 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',
), ),
migrations.AlterUniqueTogether(name="collection", unique_together=set(),),
migrations.RemoveField(model_name="collection", name="uid",),
migrations.RemoveField(model_name="collection", name="version",),
] ]

View File

@ -8,18 +8,27 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0016_auto_20200623_0820'), ("django_etebase", "0016_auto_20200623_0820"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='collection', model_name="collection",
name='main_item', name="main_item",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent', to='django_etebase.CollectionItem'), field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="parent",
to="django_etebase.CollectionItem",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='collectionitem', model_name="collectionitem",
name='uid', 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]*$')]), field=models.CharField(
db_index=True,
max_length=43,
validators=[django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9]*$")],
),
), ),
] ]

View File

@ -7,13 +7,19 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0017_auto_20200623_0958'), ("django_etebase", "0017_auto_20200623_0958"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='collectionitem', model_name="collectionitem",
name='uid', 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\\-_]*$')]), field=models.CharField(
db_index=True,
max_length=43,
validators=[
django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9\\-_]*$")
],
),
), ),
] ]

View File

@ -7,13 +7,19 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0018_auto_20200624_0748'), ("django_etebase", "0018_auto_20200624_0748"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='collectionitemchunk', model_name="collectionitemchunk",
name='uid', name="uid",
field=models.CharField(db_index=True, max_length=60, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]*$')]), field=models.CharField(
db_index=True,
max_length=60,
validators=[
django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9\\-_]*$")
],
),
), ),
] ]

View File

@ -6,12 +6,9 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0019_auto_20200626_0748'), ("django_etebase", "0019_auto_20200626_0748"),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(model_name="collectionitemrevision", name="salt",),
model_name='collectionitemrevision',
name='salt',
),
] ]

View File

@ -8,33 +8,66 @@ import django_etebase.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0020_remove_collectionitemrevision_salt'), ("django_etebase", "0020_remove_collectionitemrevision_salt"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='collectioninvitation', model_name="collectioninvitation",
name='uid', 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,}$')]), 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,}$")
],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='collectionitem', model_name="collectionitem",
name='uid', 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,}$')]), 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,}$")
],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='collectionitemchunk', model_name="collectionitemchunk",
name='uid', name="uid",
field=models.CharField(db_index=True, max_length=60, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]{20,}$')]), field=models.CharField(
db_index=True,
max_length=60,
validators=[
django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9\\-_]{20,}$")
],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='collectionitemrevision', model_name="collectionitemrevision",
name='uid', 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,}$')]), 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,}$")
],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='stoken', model_name="stoken",
name='uid', name="uid",
field=models.CharField(db_index=True, default=django_etebase.models.generate_stoken_uid, max_length=43, unique=True, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]{20,}$')]), field=models.CharField(
db_index=True,
default=django_etebase.models.generate_stoken_uid,
max_length=43,
unique=True,
validators=[
django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9\\-_]{20,}$")
],
),
), ),
] ]

View File

@ -6,12 +6,9 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0021_auto_20200626_0913'), ("django_etebase", "0021_auto_20200626_0913"),
] ]
operations = [ operations = [
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(name="collectionitemchunk", unique_together={("item", "uid")},),
name='collectionitemchunk',
unique_together={('item', 'uid')},
),
] ]

View File

@ -7,13 +7,18 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0022_auto_20200804_1059'), ("django_etebase", "0022_auto_20200804_1059"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='collectionitemchunk', model_name="collectionitemchunk",
name='collection', name="collection",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='django_etebase.Collection'), field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="chunks",
to="django_etebase.Collection",
),
), ),
] ]

View File

@ -4,7 +4,7 @@ from django.db import migrations
def change_chunk_to_collections(apps, schema_editor): def change_chunk_to_collections(apps, schema_editor):
CollectionItemChunk = apps.get_model('django_etebase', 'CollectionItemChunk') CollectionItemChunk = apps.get_model("django_etebase", "CollectionItemChunk")
for chunk in CollectionItemChunk.objects.all(): for chunk in CollectionItemChunk.objects.all():
chunk.collection = chunk.item.collection chunk.collection = chunk.item.collection
@ -14,7 +14,7 @@ def change_chunk_to_collections(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0023_collectionitemchunk_collection'), ("django_etebase", "0023_collectionitemchunk_collection"),
] ]
operations = [ operations = [

View File

@ -7,21 +7,17 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0024_auto_20200804_1209'), ("django_etebase", "0024_auto_20200804_1209"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='collectionitemchunk', model_name="collectionitemchunk",
name='collection', name="collection",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='django_etebase.Collection'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="chunks", to="django_etebase.Collection"
), ),
migrations.AlterUniqueTogether(
name='collectionitemchunk',
unique_together={('collection', 'uid')},
),
migrations.RemoveField(
model_name='collectionitemchunk',
name='item',
), ),
migrations.AlterUniqueTogether(name="collectionitemchunk", unique_together={("collection", "uid")},),
migrations.RemoveField(model_name="collectionitemchunk", name="item",),
] ]

View File

@ -6,18 +6,10 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0025_auto_20200804_1216'), ("django_etebase", "0025_auto_20200804_1216"),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(model_name="collectioninvitation", old_name="accessLevel", new_name="accessLevelOld",),
model_name='collectioninvitation', migrations.RenameField(model_name="collectionmember", old_name="accessLevel", new_name="accessLevelOld",),
old_name='accessLevel',
new_name='accessLevelOld',
),
migrations.RenameField(
model_name='collectionmember',
old_name='accessLevel',
new_name='accessLevelOld',
),
] ]

View File

@ -6,18 +6,18 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0026_auto_20200907_0752'), ("django_etebase", "0026_auto_20200907_0752"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='collectioninvitation', model_name="collectioninvitation",
name='accessLevel', name="accessLevel",
field=models.IntegerField(choices=[(0, 'Read Only'), (1, 'Admin'), (2, 'Read Write')], default=0), field=models.IntegerField(choices=[(0, "Read Only"), (1, "Admin"), (2, "Read Write")], default=0),
), ),
migrations.AddField( migrations.AddField(
model_name='collectionmember', model_name="collectionmember",
name='accessLevel', name="accessLevel",
field=models.IntegerField(choices=[(0, 'Read Only'), (1, 'Admin'), (2, 'Read Write')], default=0), field=models.IntegerField(choices=[(0, "Read Only"), (1, "Admin"), (2, "Read Write")], default=0),
), ),
] ]

View File

@ -6,24 +6,24 @@ from django_etebase.models import AccessLevels
def change_access_level_to_int(apps, schema_editor): def change_access_level_to_int(apps, schema_editor):
CollectionMember = apps.get_model('django_etebase', 'CollectionMember') CollectionMember = apps.get_model("django_etebase", "CollectionMember")
CollectionInvitation = apps.get_model('django_etebase', 'CollectionInvitation') CollectionInvitation = apps.get_model("django_etebase", "CollectionInvitation")
for member in CollectionMember.objects.all(): for member in CollectionMember.objects.all():
if member.accessLevelOld == 'adm': if member.accessLevelOld == "adm":
member.accessLevel = AccessLevels.ADMIN member.accessLevel = AccessLevels.ADMIN
elif member.accessLevelOld == 'rw': elif member.accessLevelOld == "rw":
member.accessLevel = AccessLevels.READ_WRITE member.accessLevel = AccessLevels.READ_WRITE
elif member.accessLevelOld == 'ro': elif member.accessLevelOld == "ro":
member.accessLevel = AccessLevels.READ_ONLY member.accessLevel = AccessLevels.READ_ONLY
member.save() member.save()
for invitation in CollectionInvitation.objects.all(): for invitation in CollectionInvitation.objects.all():
if invitation.accessLevelOld == 'adm': if invitation.accessLevelOld == "adm":
invitation.accessLevel = AccessLevels.ADMIN invitation.accessLevel = AccessLevels.ADMIN
elif invitation.accessLevelOld == 'rw': elif invitation.accessLevelOld == "rw":
invitation.accessLevel = AccessLevels.READ_WRITE invitation.accessLevel = AccessLevels.READ_WRITE
elif invitation.accessLevelOld == 'ro': elif invitation.accessLevelOld == "ro":
invitation.accessLevel = AccessLevels.READ_ONLY invitation.accessLevel = AccessLevels.READ_ONLY
invitation.save() invitation.save()
@ -31,7 +31,7 @@ def change_access_level_to_int(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0027_auto_20200907_0752'), ("django_etebase", "0027_auto_20200907_0752"),
] ]
operations = [ operations = [

View File

@ -6,16 +6,10 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0028_auto_20200907_0754'), ("django_etebase", "0028_auto_20200907_0754"),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(model_name="collectioninvitation", name="accessLevelOld",),
model_name='collectioninvitation', migrations.RemoveField(model_name="collectionmember", name="accessLevelOld",),
name='accessLevelOld',
),
migrations.RemoveField(
model_name='collectionmember',
name='accessLevelOld',
),
] ]

View File

@ -7,13 +7,18 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0029_auto_20200907_0801'), ("django_etebase", "0029_auto_20200907_0801"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='collection', model_name="collection",
name='main_item', name="main_item",
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent', to='django_etebase.collectionitem'), field=models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="parent",
to="django_etebase.collectionitem",
),
), ),
] ]

View File

@ -9,21 +9,23 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('django_etebase', '0030_auto_20200922_0832'), ("django_etebase", "0030_auto_20200922_0832"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='CollectionType', name="CollectionType",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('uid', models.BinaryField(db_index=True, editable=True)), ("uid", models.BinaryField(db_index=True, editable=True)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
), ),
migrations.AddField( migrations.AddField(
model_name='collectionmember', model_name="collectionmember",
name='collectionType', name="collectionType",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etebase.collectiontype'), field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.PROTECT, to="django_etebase.collectiontype"
),
), ),
] ]

View File

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('django_etebase', '0031_auto_20201013_1336'), ("django_etebase", "0031_auto_20201013_1336"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='collectiontype', model_name="collectiontype",
name='uid', name="uid",
field=models.BinaryField(db_index=True, editable=True, unique=True), field=models.BinaryField(db_index=True, editable=True, unique=True),
), ),
] ]

View File

@ -27,7 +27,7 @@ from . import app_settings
from .exceptions import EtebaseValidationError from .exceptions import EtebaseValidationError
UidValidator = RegexValidator(regex=r'^[a-zA-Z0-9\-_]{20,}$', message='Not a valid UID') UidValidator = RegexValidator(regex=r"^[a-zA-Z0-9\-_]{20,}$", message="Not a valid UID")
class CollectionType(models.Model): class CollectionType(models.Model):
@ -36,7 +36,7 @@ class CollectionType(models.Model):
class Collection(models.Model): class Collection(models.Model):
main_item = models.OneToOneField('CollectionItem', related_name='parent', null=True, on_delete=models.SET_NULL) 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) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
def __str__(self): def __str__(self):
@ -56,38 +56,45 @@ class Collection(models.Model):
@cached_property @cached_property
def stoken(self): def stoken(self):
stoken = Stoken.objects.filter( stoken = (
Stoken.objects.filter(
Q(collectionitemrevision__item__collection=self) | Q(collectionmember__collection=self) Q(collectionitemrevision__item__collection=self) | Q(collectionmember__collection=self)
).order_by('id').last() )
.order_by("id")
.last()
)
if stoken is None: if stoken is None:
raise Exception('stoken is None. Should never happen') raise Exception("stoken is None. Should never happen")
return stoken.uid return stoken.uid
def validate_unique(self, exclude=None): def validate_unique(self, exclude=None):
super().validate_unique(exclude=exclude) super().validate_unique(exclude=exclude)
if exclude is None or 'main_item' in exclude: if exclude is None or "main_item" in exclude:
return return
if self.__class__.objects.filter(owner=self.owner, main_item__uid=self.main_item.uid) \ if (
.exclude(id=self.id).exists(): self.__class__.objects.filter(owner=self.owner, main_item__uid=self.main_item.uid)
raise EtebaseValidationError('unique_uid', 'Collection with this uid already exists', .exclude(id=self.id)
status_code=status.HTTP_409_CONFLICT) .exists()
):
raise EtebaseValidationError(
"unique_uid", "Collection with this uid already exists", status_code=status.HTTP_409_CONFLICT
)
class CollectionItem(models.Model): class CollectionItem(models.Model):
uid = models.CharField(db_index=True, blank=False, uid = models.CharField(db_index=True, blank=False, max_length=43, validators=[UidValidator])
max_length=43, validators=[UidValidator]) collection = models.ForeignKey(Collection, related_name="items", on_delete=models.CASCADE)
collection = models.ForeignKey(Collection, related_name='items', on_delete=models.CASCADE)
version = models.PositiveSmallIntegerField() version = models.PositiveSmallIntegerField()
encryptionKey = models.BinaryField(editable=True, blank=False, null=True) encryptionKey = models.BinaryField(editable=True, blank=False, null=True)
class Meta: class Meta:
unique_together = ('uid', 'collection') unique_together = ("uid", "collection")
def __str__(self): def __str__(self):
return '{} {}'.format(self.uid, self.collection.uid) return "{} {}".format(self.uid, self.collection.uid)
@cached_property @cached_property
def content(self): def content(self):
@ -107,53 +114,60 @@ def chunk_directory_path(instance, filename):
user_id = col.owner.id user_id = col.owner.id
uid_prefix = instance.uid[:2] uid_prefix = instance.uid[:2]
uid_rest = instance.uid[2:] uid_rest = instance.uid[2:]
return Path('user_{}'.format(user_id), col.uid, uid_prefix, uid_rest) return Path("user_{}".format(user_id), col.uid, uid_prefix, uid_rest)
class CollectionItemChunk(models.Model): class CollectionItemChunk(models.Model):
uid = models.CharField(db_index=True, blank=False, null=False, uid = models.CharField(db_index=True, blank=False, null=False, max_length=60, validators=[UidValidator])
max_length=60, validators=[UidValidator]) collection = models.ForeignKey(Collection, related_name="chunks", on_delete=models.CASCADE)
collection = models.ForeignKey(Collection, related_name='chunks', on_delete=models.CASCADE)
chunkFile = models.FileField(upload_to=chunk_directory_path, max_length=150, unique=True) chunkFile = models.FileField(upload_to=chunk_directory_path, max_length=150, unique=True)
def __str__(self): def __str__(self):
return self.uid return self.uid
class Meta: class Meta:
unique_together = ('collection', 'uid') unique_together = ("collection", "uid")
def generate_stoken_uid(): def generate_stoken_uid():
return get_random_string(32, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_') return get_random_string(32, allowed_chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_")
class Stoken(models.Model): class Stoken(models.Model):
uid = models.CharField(db_index=True, unique=True, blank=False, null=False, default=generate_stoken_uid, uid = models.CharField(
max_length=43, validators=[UidValidator]) db_index=True,
unique=True,
blank=False,
null=False,
default=generate_stoken_uid,
max_length=43,
validators=[UidValidator],
)
class CollectionItemRevision(models.Model): class CollectionItemRevision(models.Model):
stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT) stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT)
uid = models.CharField(db_index=True, unique=True, blank=False, null=False, uid = models.CharField(
max_length=43, validators=[UidValidator]) db_index=True, unique=True, blank=False, null=False, max_length=43, validators=[UidValidator]
item = models.ForeignKey(CollectionItem, related_name='revisions', on_delete=models.CASCADE) )
item = models.ForeignKey(CollectionItem, related_name="revisions", on_delete=models.CASCADE)
meta = models.BinaryField(editable=True, blank=False, null=False) meta = models.BinaryField(editable=True, blank=False, null=False)
current = models.BooleanField(db_index=True, default=True, null=True) current = models.BooleanField(db_index=True, default=True, null=True)
deleted = models.BooleanField(default=False) deleted = models.BooleanField(default=False)
class Meta: class Meta:
unique_together = ('item', 'current') unique_together = ("item", "current")
def __str__(self): def __str__(self):
return '{} {} current={}'.format(self.uid, self.item.uid, self.current) return "{} {} current={}".format(self.uid, self.item.uid, self.current)
class RevisionChunkRelation(models.Model): class RevisionChunkRelation(models.Model):
chunk = models.ForeignKey(CollectionItemChunk, related_name='revisions_relation', on_delete=models.CASCADE) chunk = models.ForeignKey(CollectionItemChunk, related_name="revisions_relation", on_delete=models.CASCADE)
revision = models.ForeignKey(CollectionItemRevision, related_name='chunks_relation', on_delete=models.CASCADE) revision = models.ForeignKey(CollectionItemRevision, related_name="chunks_relation", on_delete=models.CASCADE)
class Meta: class Meta:
ordering = ('id', ) ordering = ("id",)
class AccessLevels(models.IntegerChoices): class AccessLevels(models.IntegerChoices):
@ -164,28 +178,22 @@ class AccessLevels(models.IntegerChoices):
class CollectionMember(models.Model): class CollectionMember(models.Model):
stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT, null=True) stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT, null=True)
collection = models.ForeignKey(Collection, related_name='members', on_delete=models.CASCADE) collection = models.ForeignKey(Collection, related_name="members", on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
encryptionKey = models.BinaryField(editable=True, blank=False, null=False) encryptionKey = models.BinaryField(editable=True, blank=False, null=False)
collectionType = models.ForeignKey(CollectionType, on_delete=models.PROTECT, null=True) collectionType = models.ForeignKey(CollectionType, on_delete=models.PROTECT, null=True)
accessLevel = models.IntegerField( accessLevel = models.IntegerField(choices=AccessLevels.choices, default=AccessLevels.READ_ONLY,)
choices=AccessLevels.choices,
default=AccessLevels.READ_ONLY,
)
class Meta: class Meta:
unique_together = ('user', 'collection') unique_together = ("user", "collection")
def __str__(self): def __str__(self):
return '{} {}'.format(self.collection.uid, self.user) return "{} {}".format(self.collection.uid, self.user)
def revoke(self): def revoke(self):
with transaction.atomic(): with transaction.atomic():
CollectionMemberRemoved.objects.update_or_create( CollectionMemberRemoved.objects.update_or_create(
collection=self.collection, user=self.user, collection=self.collection, user=self.user, defaults={"stoken": Stoken.objects.create(),},
defaults={
'stoken': Stoken.objects.create(),
},
) )
self.delete() self.delete()
@ -193,36 +201,32 @@ class CollectionMember(models.Model):
class CollectionMemberRemoved(models.Model): class CollectionMemberRemoved(models.Model):
stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT, null=True) stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT, null=True)
collection = models.ForeignKey(Collection, related_name='removed_members', on_delete=models.CASCADE) collection = models.ForeignKey(Collection, related_name="removed_members", on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Meta: class Meta:
unique_together = ('user', 'collection') unique_together = ("user", "collection")
def __str__(self): def __str__(self):
return '{} {}'.format(self.collection.uid, self.user) return "{} {}".format(self.collection.uid, self.user)
class CollectionInvitation(models.Model): class CollectionInvitation(models.Model):
uid = models.CharField(db_index=True, blank=False, null=False, uid = models.CharField(db_index=True, blank=False, null=False, max_length=43, validators=[UidValidator])
max_length=43, validators=[UidValidator])
version = models.PositiveSmallIntegerField(default=1) version = models.PositiveSmallIntegerField(default=1)
fromMember = models.ForeignKey(CollectionMember, on_delete=models.CASCADE) fromMember = models.ForeignKey(CollectionMember, on_delete=models.CASCADE)
# FIXME: make sure to delete all invitations for the same collection once one is accepted # FIXME: make sure to delete all invitations for the same collection once one is accepted
# Make sure to not allow invitations if already a member # Make sure to not allow invitations if already a member
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='incoming_invitations', on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="incoming_invitations", on_delete=models.CASCADE)
signedEncryptionKey = models.BinaryField(editable=False, blank=False, null=False) signedEncryptionKey = models.BinaryField(editable=False, blank=False, null=False)
accessLevel = models.IntegerField( accessLevel = models.IntegerField(choices=AccessLevels.choices, default=AccessLevels.READ_ONLY,)
choices=AccessLevels.choices,
default=AccessLevels.READ_ONLY,
)
class Meta: class Meta:
unique_together = ('user', 'fromMember') unique_together = ("user", "fromMember")
def __str__(self): def __str__(self):
return '{} {}'.format(self.fromMember.collection.uid, self.user) return "{} {}".format(self.fromMember.collection.uid, self.user)
@cached_property @cached_property
def collection(self): def collection(self):

View File

@ -5,11 +5,12 @@ class ChunkUploadParser(FileUploadParser):
""" """
Parser for chunk upload data. Parser for chunk upload data.
""" """
media_type = 'application/octet-stream'
media_type = "application/octet-stream"
def get_filename(self, stream, media_type, parser_context): def get_filename(self, stream, media_type, parser_context):
""" """
Detects the uploaded file name. Detects the uploaded file name.
""" """
view = parser_context['view'] view = parser_context["view"]
return parser_context['kwargs'][view.lookup_field] return parser_context["kwargs"][view.lookup_field]

View File

@ -25,13 +25,14 @@ class IsCollectionAdmin(permissions.BasePermission):
""" """
Custom permission to only allow owners of a collection to view it Custom permission to only allow owners of a collection to view it
""" """
message = { message = {
'detail': 'Only collection admins can perform this operation.', "detail": "Only collection admins can perform this operation.",
'code': 'admin_access_required', "code": "admin_access_required",
} }
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(main_item__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)
@ -44,13 +45,14 @@ class IsCollectionAdminOrReadOnly(permissions.BasePermission):
""" """
Custom permission to only allow owners of a collection to edit it Custom permission to only allow owners of a collection to edit it
""" """
message = { message = {
'detail': 'Only collection admins can edit collections.', "detail": "Only collection admins can edit collections.",
'code': 'admin_access_required', "code": "admin_access_required",
} }
def has_permission(self, request, view): def has_permission(self, request, view):
collection_uid = view.kwargs.get('collection_uid', None) collection_uid = view.kwargs.get("collection_uid", None)
# Allow creating new collections # Allow creating new collections
if collection_uid is None: if collection_uid is None:
@ -71,13 +73,14 @@ class HasWriteAccessOrReadOnly(permissions.BasePermission):
""" """
Custom permission to restrict write Custom permission to restrict write
""" """
message = { message = {
'detail': 'You need write access to write to this collection', "detail": "You need write access to write to this collection",
'code': 'no_write_access', "code": "no_write_access",
} }
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(main_item__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:

View File

@ -15,4 +15,5 @@ class JSONRenderer(DRFJSONRenderer):
""" """
Renderer which serializes to JSON with support for our base64 Renderer which serializes to JSON with support for our base64
""" """
encoder_class = JSONEncoder encoder_class = JSONEncoder

View File

@ -29,7 +29,7 @@ User = get_user_model()
def process_revisions_for_item(item, revision_data): def process_revisions_for_item(item, revision_data):
chunks_objs = [] chunks_objs = []
chunks = revision_data.pop('chunks_relation') chunks = revision_data.pop("chunks_relation")
revision = models.CollectionItemRevision(**revision_data, item=item) revision = models.CollectionItemRevision(**revision_data, item=item)
revision.validate_unique() # Verify there aren't any validation issues revision.validate_unique() # Verify there aren't any validation issues
@ -42,11 +42,11 @@ def process_revisions_for_item(item, revision_data):
# If the chunk already exists we assume it's fine. Otherwise, we upload it. # If the chunk already exists we assume it's fine. Otherwise, we upload it.
if chunk_obj is None: if chunk_obj is None:
chunk_obj = models.CollectionItemChunk(uid=uid, collection=item.collection) chunk_obj = models.CollectionItemChunk(uid=uid, collection=item.collection)
chunk_obj.chunkFile.save('IGNORED', ContentFile(content)) chunk_obj.chunkFile.save("IGNORED", ContentFile(content))
chunk_obj.save() chunk_obj.save()
else: else:
if chunk_obj is None: if chunk_obj is None:
raise EtebaseValidationError('chunk_no_content', 'Tried to create a new chunk without content') raise EtebaseValidationError("chunk_no_content", "Tried to create a new chunk without content")
chunks_objs.append(chunk_obj) chunks_objs.append(chunk_obj)
@ -60,7 +60,7 @@ def process_revisions_for_item(item, revision_data):
def b64encode(value): def b64encode(value):
return base64.urlsafe_b64encode(value).decode('ascii').strip('=') return base64.urlsafe_b64encode(value).decode("ascii").strip("=")
def b64decode(data): def b64decode(data):
@ -85,7 +85,7 @@ class BinaryBase64Field(serializers.Field):
class CollectionEncryptionKeyField(BinaryBase64Field): class CollectionEncryptionKeyField(BinaryBase64Field):
def get_attribute(self, instance): def get_attribute(self, instance):
request = self.context.get('request', None) request = self.context.get("request", None)
if request is not None: if request is not None:
return instance.members.get(user=request.user).encryptionKey return instance.members.get(user=request.user).encryptionKey
return None return None
@ -93,7 +93,7 @@ class CollectionEncryptionKeyField(BinaryBase64Field):
class CollectionTypeField(BinaryBase64Field): class CollectionTypeField(BinaryBase64Field):
def get_attribute(self, instance): def get_attribute(self, instance):
request = self.context.get('request', None) request = self.context.get("request", None)
if request is not None: if request is not None:
collection_type = instance.members.get(user=request.user).collectionType collection_type = instance.members.get(user=request.user).collectionType
return collection_type and collection_type.uid return collection_type and collection_type.uid
@ -102,7 +102,7 @@ class CollectionTypeField(BinaryBase64Field):
class UserSlugRelatedField(serializers.SlugRelatedField): class UserSlugRelatedField(serializers.SlugRelatedField):
def get_queryset(self): def get_queryset(self):
view = self.context.get('view', None) view = self.context.get("view", None)
return get_user_queryset(super().get_queryset(), view) return get_user_queryset(super().get_queryset(), view)
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -115,15 +115,15 @@ class UserSlugRelatedField(serializers.SlugRelatedField):
class ChunksField(serializers.RelatedField): class ChunksField(serializers.RelatedField):
def to_representation(self, obj): def to_representation(self, obj):
obj = obj.chunk obj = obj.chunk
if self.context.get('prefetch') == 'auto': if self.context.get("prefetch") == "auto":
with open(obj.chunkFile.path, 'rb') as f: with open(obj.chunkFile.path, "rb") as f:
return (obj.uid, f.read()) return (obj.uid, f.read())
else: else:
return (obj.uid, ) return (obj.uid,)
def to_internal_value(self, data): def to_internal_value(self, data):
if data[0] is None or data[1] is None: if data[0] is None or data[1] is None:
raise EtebaseValidationError('no_null', 'null is not allowed') raise EtebaseValidationError("no_null", "null is not allowed")
return (data[0], b64decode_or_bytes(data[1])) return (data[0], b64decode_or_bytes(data[1]))
@ -133,18 +133,12 @@ class BetterErrorsMixin:
nice = [] nice = []
errors = super().errors errors = super().errors
for error_type in errors: for error_type in errors:
if error_type == 'non_field_errors': if error_type == "non_field_errors":
nice.extend( nice.extend(self.flatten_errors(None, errors[error_type]))
self.flatten_errors(None, errors[error_type])
)
else: else:
nice.extend( nice.extend(self.flatten_errors(error_type, errors[error_type]))
self.flatten_errors(error_type, errors[error_type])
)
if nice: if nice:
return {'code': 'field_errors', return {"code": "field_errors", "detail": "Field validations failed.", "errors": nice}
'detail': 'Field validations failed.',
'errors': nice}
return {} return {}
def flatten_errors(self, field_name, errors): def flatten_errors(self, field_name, errors):
@ -155,54 +149,50 @@ class BetterErrorsMixin:
ret.extend(self.flatten_errors("{}.{}".format(field_name, error_key), error)) ret.extend(self.flatten_errors("{}.{}".format(field_name, error_key), error))
else: else:
for error in errors: for error in errors:
if hasattr(error, 'detail'): if hasattr(error, "detail"):
message = error.detail[0] message = error.detail[0]
elif hasattr(error, 'message'): elif hasattr(error, "message"):
message = error.message message = error.message
else: else:
message = str(error) message = str(error)
ret.append({ ret.append(
'field': field_name, {"field": field_name, "code": error.code, "detail": message,}
'code': error.code, )
'detail': message,
})
return ret return ret
def transform_validation_error(self, prefix, err): def transform_validation_error(self, prefix, err):
if hasattr(err, 'error_dict'): if hasattr(err, "error_dict"):
errors = self.flatten_errors(prefix, err.error_dict) errors = self.flatten_errors(prefix, err.error_dict)
elif not hasattr(err, 'message'): elif not hasattr(err, "message"):
errors = self.flatten_errors(prefix, err.error_list) errors = self.flatten_errors(prefix, err.error_list)
else: else:
raise EtebaseValidationError(err.code, err.message) raise EtebaseValidationError(err.code, err.message)
raise serializers.ValidationError({ raise serializers.ValidationError(
'code': 'field_errors', {"code": "field_errors", "detail": "Field validations failed.", "errors": errors,}
'detail': 'Field validations failed.', )
'errors': errors,
})
class CollectionItemChunkSerializer(BetterErrorsMixin, serializers.ModelSerializer): class CollectionItemChunkSerializer(BetterErrorsMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = models.CollectionItemChunk model = models.CollectionItemChunk
fields = ('uid', 'chunkFile') fields = ("uid", "chunkFile")
class CollectionItemRevisionSerializer(BetterErrorsMixin, serializers.ModelSerializer): class CollectionItemRevisionSerializer(BetterErrorsMixin, serializers.ModelSerializer):
chunks = ChunksField( chunks = ChunksField(
source='chunks_relation', source="chunks_relation",
queryset=models.RevisionChunkRelation.objects.all(), queryset=models.RevisionChunkRelation.objects.all(),
style={'base_template': 'input.html'}, style={"base_template": "input.html"},
many=True many=True,
) )
meta = BinaryBase64Field() meta = BinaryBase64Field()
class Meta: class Meta:
model = models.CollectionItemRevision model = models.CollectionItemRevision
fields = ('chunks', 'meta', 'uid', 'deleted') fields = ("chunks", "meta", "uid", "deleted")
extra_kwargs = { extra_kwargs = {
'uid': {'validators': []}, # We deal with it in the serializers "uid": {"validators": []}, # We deal with it in the serializers
} }
@ -213,14 +203,14 @@ class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = models.CollectionItem model = models.CollectionItem
fields = ('uid', 'version', 'encryptionKey', 'content', 'etag') fields = ("uid", "version", "encryptionKey", "content", "etag")
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"""
validate_etag = self.context.get('validate_etag', False) validate_etag = self.context.get("validate_etag", False)
etag = validated_data.pop('etag') etag = validated_data.pop("etag")
revision_data = validated_data.pop('content') revision_data = validated_data.pop("content")
uid = validated_data.pop('uid') uid = validated_data.pop("uid")
Model = self.__class__.Meta.model Model = self.__class__.Meta.model
@ -229,12 +219,15 @@ class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer):
cur_etag = instance.etag if not created else None cur_etag = instance.etag if not created else None
# If we are trying to update an up to date item, abort early and consider it a success # If we are trying to update an up to date item, abort early and consider it a success
if cur_etag == revision_data.get('uid'): if cur_etag == revision_data.get("uid"):
return instance return instance
if validate_etag and cur_etag != etag: if validate_etag and cur_etag != etag:
raise EtebaseValidationError('wrong_etag', 'Wrong etag. Expected {} got {}'.format(cur_etag, etag), raise EtebaseValidationError(
status_code=status.HTTP_409_CONFLICT) "wrong_etag",
"Wrong etag. Expected {} got {}".format(cur_etag, etag),
status_code=status.HTTP_409_CONFLICT,
)
if not created: if not created:
# We don't have to use select_for_update here because the unique constraint on current guards against # We don't have to use select_for_update here because the unique constraint on current guards against
@ -260,14 +253,17 @@ class CollectionItemDepSerializer(BetterErrorsMixin, serializers.ModelSerializer
class Meta: class Meta:
model = models.CollectionItem model = models.CollectionItem
fields = ('uid', 'etag') fields = ("uid", "etag")
def validate(self, data): def validate(self, data):
item = self.__class__.Meta.model.objects.get(uid=data['uid']) item = self.__class__.Meta.model.objects.get(uid=data["uid"])
etag = data['etag'] etag = data["etag"]
if item.etag != etag: if item.etag != etag:
raise EtebaseValidationError('wrong_etag', 'Wrong etag. Expected {} got {}'.format(item.etag, etag), raise EtebaseValidationError(
status_code=status.HTTP_409_CONFLICT) "wrong_etag",
"Wrong etag. Expected {} got {}".format(item.etag, etag),
status_code=status.HTTP_409_CONFLICT,
)
return data return data
@ -277,49 +273,47 @@ class CollectionItemBulkGetSerializer(BetterErrorsMixin, serializers.ModelSerial
class Meta: class Meta:
model = models.CollectionItem model = models.CollectionItem
fields = ('uid', 'etag') fields = ("uid", "etag")
class CollectionListMultiSerializer(BetterErrorsMixin, serializers.Serializer): class CollectionListMultiSerializer(BetterErrorsMixin, serializers.Serializer):
collectionTypes = serializers.ListField( collectionTypes = serializers.ListField(child=BinaryBase64Field())
child=BinaryBase64Field()
)
class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer): class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer):
collectionKey = CollectionEncryptionKeyField() collectionKey = CollectionEncryptionKeyField()
collectionType = CollectionTypeField() collectionType = CollectionTypeField()
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)
item = CollectionItemSerializer(many=False, source='main_item') item = CollectionItemSerializer(many=False, source="main_item")
class Meta: class Meta:
model = models.Collection model = models.Collection
fields = ('item', 'accessLevel', 'collectionKey', 'collectionType', 'stoken') fields = ("item", "accessLevel", "collectionKey", "collectionType", "stoken")
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)
if request is not None: if request is not None:
return obj.members.get(user=request.user).accessLevel return obj.members.get(user=request.user).accessLevel
return None return None
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') collection_key = validated_data.pop("collectionKey")
collection_type = validated_data.pop('collectionType') collection_type = validated_data.pop("collectionType")
user = validated_data.get('owner') user = validated_data.get("owner")
main_item_data = validated_data.pop('main_item') main_item_data = validated_data.pop("main_item")
etag = main_item_data.pop('etag') etag = main_item_data.pop("etag")
revision_data = main_item_data.pop('content') revision_data = main_item_data.pop("content")
instance = self.__class__.Meta.model(**validated_data) instance = self.__class__.Meta.model(**validated_data)
with transaction.atomic(): with transaction.atomic():
_ = self.__class__.Meta.model.objects.select_for_update().filter(owner=user) _ = self.__class__.Meta.model.objects.select_for_update().filter(owner=user)
if etag is not None: if etag is not None:
raise EtebaseValidationError('bad_etag', 'etag is not null') raise EtebaseValidationError("bad_etag", "etag is not null")
instance.save() instance.save()
main_item = models.CollectionItem.objects.create(**main_item_data, collection=instance) main_item = models.CollectionItem.objects.create(**main_item_data, collection=instance)
@ -333,7 +327,8 @@ class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer):
collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=collection_type, owner=user) collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=collection_type, owner=user)
models.CollectionMember(collection=instance, models.CollectionMember(
collection=instance,
stoken=models.Stoken.objects.create(), stoken=models.Stoken.objects.create(),
user=user, user=user,
accessLevel=models.AccessLevels.ADMIN, accessLevel=models.AccessLevels.ADMIN,
@ -348,15 +343,11 @@ class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer):
class CollectionMemberSerializer(BetterErrorsMixin, serializers.ModelSerializer): class CollectionMemberSerializer(BetterErrorsMixin, serializers.ModelSerializer):
username = UserSlugRelatedField( username = UserSlugRelatedField(source="user", read_only=True, style={"base_template": "input.html"},)
source='user',
read_only=True,
style={'base_template': 'input.html'},
)
class Meta: class Meta:
model = models.CollectionMember model = models.CollectionMember
fields = ('username', 'accessLevel') fields = ("username", "accessLevel")
def create(self, validated_data): def create(self, validated_data):
raise NotImplementedError() raise NotImplementedError()
@ -364,7 +355,7 @@ class CollectionMemberSerializer(BetterErrorsMixin, serializers.ModelSerializer)
def update(self, instance, validated_data): def update(self, instance, validated_data):
with transaction.atomic(): with transaction.atomic():
# We only allow updating accessLevel # We only allow updating accessLevel
access_level = validated_data.pop('accessLevel') access_level = validated_data.pop("accessLevel")
if instance.accessLevel != access_level: if instance.accessLevel != access_level:
instance.stoken = models.Stoken.objects.create() instance.stoken = models.Stoken.objects.create()
instance.accessLevel = access_level instance.accessLevel = access_level
@ -374,31 +365,35 @@ class CollectionMemberSerializer(BetterErrorsMixin, serializers.ModelSerializer)
class CollectionInvitationSerializer(BetterErrorsMixin, serializers.ModelSerializer): class CollectionInvitationSerializer(BetterErrorsMixin, serializers.ModelSerializer):
username = UserSlugRelatedField( username = UserSlugRelatedField(source="user", queryset=User.objects, style={"base_template": "input.html"},)
source='user', collection = serializers.CharField(source="collection.uid")
queryset=User.objects, fromUsername = BinaryBase64Field(source="fromMember.user.username", read_only=True)
style={'base_template': 'input.html'}, fromPubkey = BinaryBase64Field(source="fromMember.user.userinfo.pubkey", read_only=True)
)
collection = serializers.CharField(source='collection.uid')
fromUsername = BinaryBase64Field(source='fromMember.user.username', read_only=True)
fromPubkey = BinaryBase64Field(source='fromMember.user.userinfo.pubkey', read_only=True)
signedEncryptionKey = BinaryBase64Field() signedEncryptionKey = BinaryBase64Field()
class Meta: class Meta:
model = models.CollectionInvitation model = models.CollectionInvitation
fields = ('username', 'uid', 'collection', 'signedEncryptionKey', 'accessLevel', fields = (
'fromUsername', 'fromPubkey', 'version') "username",
"uid",
"collection",
"signedEncryptionKey",
"accessLevel",
"fromUsername",
"fromPubkey",
"version",
)
def validate_user(self, value): def validate_user(self, value):
request = self.context['request'] request = self.context["request"]
if request.user.username == value.lower(): if request.user.username == value.lower():
raise EtebaseValidationError('no_self_invite', 'Inviting yourself is not allowed') raise EtebaseValidationError("no_self_invite", "Inviting yourself is not allowed")
return value return value
def create(self, validated_data): def create(self, validated_data):
request = self.context['request'] request = self.context["request"]
collection = validated_data.pop('collection') collection = validated_data.pop("collection")
member = collection.members.get(user=request.user) member = collection.members.get(user=request.user)
@ -406,12 +401,12 @@ class CollectionInvitationSerializer(BetterErrorsMixin, serializers.ModelSeriali
try: try:
return type(self).Meta.model.objects.create(**validated_data, fromMember=member) return type(self).Meta.model.objects.create(**validated_data, fromMember=member)
except IntegrityError: except IntegrityError:
raise EtebaseValidationError('invitation_exists', 'Invitation already exists') raise EtebaseValidationError("invitation_exists", "Invitation already exists")
def update(self, instance, validated_data): def update(self, instance, validated_data):
with transaction.atomic(): with transaction.atomic():
instance.accessLevel = validated_data.pop('accessLevel') instance.accessLevel = validated_data.pop("accessLevel")
instance.signedEncryptionKey = validated_data.pop('signedEncryptionKey') instance.signedEncryptionKey = validated_data.pop("signedEncryptionKey")
instance.save() instance.save()
return instance return instance
@ -424,9 +419,9 @@ class InvitationAcceptSerializer(BetterErrorsMixin, serializers.Serializer):
def create(self, validated_data): def create(self, validated_data):
with transaction.atomic(): with transaction.atomic():
invitation = self.context['invitation'] invitation = self.context["invitation"]
encryption_key = validated_data.get('encryptionKey') encryption_key = validated_data.get("encryptionKey")
collection_type = validated_data.pop('collectionType') collection_type = validated_data.pop("collectionType")
user = invitation.user user = invitation.user
collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=collection_type, owner=user) collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=collection_type, owner=user)
@ -441,7 +436,8 @@ class InvitationAcceptSerializer(BetterErrorsMixin, serializers.Serializer):
) )
models.CollectionMemberRemoved.objects.filter( models.CollectionMemberRemoved.objects.filter(
user=invitation.user, collection=invitation.collection).delete() user=invitation.user, collection=invitation.collection
).delete()
invitation.delete() invitation.delete()
@ -452,12 +448,12 @@ class InvitationAcceptSerializer(BetterErrorsMixin, serializers.Serializer):
class UserSerializer(BetterErrorsMixin, serializers.ModelSerializer): class UserSerializer(BetterErrorsMixin, serializers.ModelSerializer):
pubkey = BinaryBase64Field(source='userinfo.pubkey') pubkey = BinaryBase64Field(source="userinfo.pubkey")
encryptedContent = BinaryBase64Field(source='userinfo.encryptedContent') encryptedContent = BinaryBase64Field(source="userinfo.encryptedContent")
class Meta: class Meta:
model = User model = User
fields = (User.USERNAME_FIELD, User.EMAIL_FIELD, 'pubkey', 'encryptedContent') fields = (User.USERNAME_FIELD, User.EMAIL_FIELD, "pubkey", "encryptedContent")
class UserInfoPubkeySerializer(BetterErrorsMixin, serializers.ModelSerializer): class UserInfoPubkeySerializer(BetterErrorsMixin, serializers.ModelSerializer):
@ -465,7 +461,7 @@ class UserInfoPubkeySerializer(BetterErrorsMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = models.UserInfo model = models.UserInfo
fields = ('pubkey', ) fields = ("pubkey",)
class UserSignupSerializer(BetterErrorsMixin, serializers.ModelSerializer): class UserSignupSerializer(BetterErrorsMixin, serializers.ModelSerializer):
@ -473,7 +469,7 @@ class UserSignupSerializer(BetterErrorsMixin, serializers.ModelSerializer):
model = User model = User
fields = (User.USERNAME_FIELD, User.EMAIL_FIELD) fields = (User.USERNAME_FIELD, User.EMAIL_FIELD)
extra_kwargs = { extra_kwargs = {
'username': {'validators': []}, # We specifically validate in SignupSerializer "username": {"validators": []}, # We specifically validate in SignupSerializer
} }
@ -481,6 +477,7 @@ class AuthenticationSignupSerializer(BetterErrorsMixin, serializers.Serializer):
"""Used both for creating new accounts and setting up existing ones for the first time. """Used both for creating new accounts and setting up existing ones for the first time.
When setting up existing ones the email is ignored." When setting up existing ones the email is ignored."
""" """
user = UserSignupSerializer(many=False) user = UserSignupSerializer(many=False)
salt = BinaryBase64Field() salt = BinaryBase64Field()
loginPubkey = BinaryBase64Field() loginPubkey = BinaryBase64Field()
@ -489,27 +486,27 @@ class AuthenticationSignupSerializer(BetterErrorsMixin, serializers.Serializer):
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"""
user_data = validated_data.pop('user') user_data = validated_data.pop("user")
with transaction.atomic(): with transaction.atomic():
try: try:
view = self.context.get('view', None) view = self.context.get("view", None)
user_queryset = get_user_queryset(User.objects.all(), view) user_queryset = get_user_queryset(User.objects.all(), view)
instance = user_queryset.get(**{User.USERNAME_FIELD: user_data['username'].lower()}) instance = user_queryset.get(**{User.USERNAME_FIELD: user_data["username"].lower()})
except User.DoesNotExist: except User.DoesNotExist:
# Create the user and save the casing the user chose as the first name # Create the user and save the casing the user chose as the first name
try: try:
instance = create_user(**user_data, password=None, first_name=user_data['username'], view=view) instance = create_user(**user_data, password=None, first_name=user_data["username"], view=view)
instance.clean_fields() instance.clean_fields()
except EtebaseValidationError as e: except EtebaseValidationError as e:
raise e raise e
except django_exceptions.ValidationError as e: except django_exceptions.ValidationError as e:
self.transform_validation_error("user", e) self.transform_validation_error("user", e)
except Exception as e: except Exception as e:
raise EtebaseValidationError('generic', str(e)) raise EtebaseValidationError("generic", str(e))
if hasattr(instance, 'userinfo'): if hasattr(instance, "userinfo"):
raise EtebaseValidationError('user_exists', 'User already exists', status_code=status.HTTP_409_CONFLICT) raise EtebaseValidationError("user_exists", "User already exists", status_code=status.HTTP_409_CONFLICT)
models.UserInfo.objects.create(**validated_data, owner=instance) models.UserInfo.objects.create(**validated_data, owner=instance)
@ -558,15 +555,15 @@ class AuthenticationChangePasswordInnerSerializer(AuthenticationLoginInnerSerial
class Meta: class Meta:
model = models.UserInfo model = models.UserInfo
fields = ('loginPubkey', 'encryptedContent') fields = ("loginPubkey", "encryptedContent")
def create(self, validated_data): def create(self, validated_data):
raise NotImplementedError() raise NotImplementedError()
def update(self, instance, validated_data): def update(self, instance, validated_data):
with transaction.atomic(): with transaction.atomic():
instance.loginPubkey = validated_data.pop('loginPubkey') instance.loginPubkey = validated_data.pop("loginPubkey")
instance.encryptedContent = validated_data.pop('encryptedContent') instance.encryptedContent = validated_data.pop("encryptedContent")
instance.save() instance.save()
return instance return instance

View File

@ -1,3 +1,3 @@
from django.dispatch import Signal from django.dispatch import Signal
user_signed_up = Signal(providing_args=['request', 'user']) user_signed_up = Signal(providing_args=["request", "user"])

View File

@ -2,4 +2,4 @@ from django.apps import AppConfig
class TokenAuthConfig(AppConfig): class TokenAuthConfig(AppConfig):
name = 'django_etebase.token_auth' name = "django_etebase.token_auth"

View File

@ -12,19 +12,19 @@ MIN_REFRESH_INTERVAL = 60
class TokenAuthentication(DRFTokenAuthentication): class TokenAuthentication(DRFTokenAuthentication):
keyword = 'Token' keyword = "Token"
model = AuthToken model = AuthToken
def authenticate_credentials(self, key): def authenticate_credentials(self, key):
msg = _('Invalid token.') msg = _("Invalid token.")
model = self.get_model() model = self.get_model()
try: try:
token = model.objects.select_related('user').get(key=key) token = model.objects.select_related("user").get(key=key)
except model.DoesNotExist: except model.DoesNotExist:
raise exceptions.AuthenticationFailed(msg) raise exceptions.AuthenticationFailed(msg)
if not token.user.is_active: if not token.user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) raise exceptions.AuthenticationFailed(_("User inactive or deleted."))
if token.expiry is not None: if token.expiry is not None:
if token.expiry < timezone.now(): if token.expiry < timezone.now():
@ -43,4 +43,4 @@ class TokenAuthentication(DRFTokenAuthentication):
delta = (new_expiry - current_expiry).total_seconds() delta = (new_expiry - current_expiry).total_seconds()
if delta > MIN_REFRESH_INTERVAL: if delta > MIN_REFRESH_INTERVAL:
auth_token.expiry = new_expiry auth_token.expiry = new_expiry
auth_token.save(update_fields=('expiry',)) auth_token.save(update_fields=("expiry",))

View File

@ -16,13 +16,23 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='AuthToken', name="AuthToken",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('key', models.CharField(db_index=True, default=token_auth_models.generate_key, max_length=40, unique=True)), (
('created', models.DateTimeField(auto_now_add=True)), "key",
('expiry', models.DateTimeField(blank=True, default=token_auth_models.get_default_expiry, null=True)), models.CharField(db_index=True, default=token_auth_models.generate_key, max_length=40, unique=True),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auth_token_set', to=settings.AUTH_USER_MODEL)), ),
("created", models.DateTimeField(auto_now_add=True)),
("expiry", models.DateTimeField(blank=True, default=token_auth_models.get_default_expiry, null=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="auth_token_set",
to=settings.AUTH_USER_MODEL,
),
),
], ],
), ),
] ]

View File

@ -17,10 +17,9 @@ def get_default_expiry():
class AuthToken(models.Model): class AuthToken(models.Model):
key = models.CharField(max_length=40, unique=True, db_index=True, default=generate_key) key = models.CharField(max_length=40, unique=True, db_index=True, default=generate_key)
user = models.ForeignKey(User, null=False, blank=False, user = models.ForeignKey(User, null=False, blank=False, related_name="auth_token_set", on_delete=models.CASCADE)
related_name='auth_token_set', on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
expiry = models.DateTimeField(null=True, blank=True, default=get_default_expiry) expiry = models.DateTimeField(null=True, blank=True, default=get_default_expiry)
def __str__(self): def __str__(self):
return '{}: {}'.format(self.key, self.user) return "{}: {}".format(self.key, self.user)

View File

@ -7,24 +7,24 @@ from rest_framework_nested import routers
from django_etebase import views from django_etebase import views
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'collection', views.CollectionViewSet) router.register(r"collection", views.CollectionViewSet)
router.register(r'authentication', views.AuthenticationViewSet, basename='authentication') router.register(r"authentication", views.AuthenticationViewSet, basename="authentication")
router.register(r'invitation/incoming', views.InvitationIncomingViewSet, basename='invitation_incoming') router.register(r"invitation/incoming", views.InvitationIncomingViewSet, basename="invitation_incoming")
router.register(r'invitation/outgoing', views.InvitationOutgoingViewSet, basename='invitation_outgoing') router.register(r"invitation/outgoing", views.InvitationOutgoingViewSet, basename="invitation_outgoing")
collections_router = routers.NestedSimpleRouter(router, r'collection', lookup='collection') collections_router = routers.NestedSimpleRouter(router, r"collection", lookup="collection")
collections_router.register(r'item', views.CollectionItemViewSet, basename='collection_item') collections_router.register(r"item", views.CollectionItemViewSet, basename="collection_item")
collections_router.register(r'member', views.CollectionMemberViewSet, basename='collection_member') collections_router.register(r"member", views.CollectionMemberViewSet, basename="collection_member")
item_router = routers.NestedSimpleRouter(collections_router, r'item', lookup='collection_item') item_router = routers.NestedSimpleRouter(collections_router, r"item", lookup="collection_item")
item_router.register(r'chunk', views.CollectionItemChunkViewSet, basename='collection_items_chunk') item_router.register(r"chunk", views.CollectionItemChunkViewSet, basename="collection_items_chunk")
if settings.DEBUG: if settings.DEBUG:
router.register(r'test/authentication', views.TestAuthenticationViewSet, basename='test_authentication') router.register(r"test/authentication", views.TestAuthenticationViewSet, basename="test_authentication")
app_name = 'django_etebase' app_name = "django_etebase"
urlpatterns = [ urlpatterns = [
path('v1/', include(router.urls)), path("v1/", include(router.urls)),
path('v1/', include(collections_router.urls)), path("v1/", include(collections_router.urls)),
path('v1/', include(item_router.urls)), path("v1/", include(item_router.urls)),
] ]

View File

@ -18,9 +18,9 @@ def create_user(*args, **kwargs):
custom_func = app_settings.CREATE_USER_FUNC custom_func = app_settings.CREATE_USER_FUNC
if custom_func is not None: if custom_func is not None:
return custom_func(*args, **kwargs) return custom_func(*args, **kwargs)
_ = kwargs.pop('view') _ = kwargs.pop("view")
return User.objects.create_user(*args, **kwargs) return User.objects.create_user(*args, **kwargs)
def create_user_blocked(*args, **kwargs): def create_user_blocked(*args, **kwargs):
raise PermissionDenied('Signup is disabled for this server. Please refer to the README for more information.') raise PermissionDenied("Signup is disabled for this server. Please refer to the README for more information.")

View File

@ -53,7 +53,7 @@ from .models import (
CollectionInvitation, CollectionInvitation,
Stoken, Stoken,
UserInfo, UserInfo,
) )
from .serializers import ( from .serializers import (
AuthenticationChangePasswordInnerSerializer, AuthenticationChangePasswordInnerSerializer,
AuthenticationSignupSerializer, AuthenticationSignupSerializer,
@ -72,7 +72,7 @@ from .serializers import (
InvitationAcceptSerializer, InvitationAcceptSerializer,
UserInfoPubkeySerializer, UserInfoPubkeySerializer,
UserSerializer, UserSerializer,
) )
from .utils import get_user_queryset from .utils import get_user_queryset
from .exceptions import EtebaseValidationError from .exceptions import EtebaseValidationError
from .parsers import ChunkUploadParser from .parsers import ChunkUploadParser
@ -99,8 +99,8 @@ class BaseViewSet(viewsets.ModelViewSet):
def get_serializer_class(self): def get_serializer_class(self):
serializer_class = self.serializer_class serializer_class = self.serializer_class
if self.request.method == 'PUT': if self.request.method == "PUT":
serializer_class = getattr(self, 'serializer_update_class', serializer_class) serializer_class = getattr(self, "serializer_update_class", serializer_class)
return serializer_class return serializer_class
@ -109,7 +109,7 @@ class BaseViewSet(viewsets.ModelViewSet):
return queryset.filter(members__user=user) return queryset.filter(members__user=user)
def get_stoken_obj_id(self, request): def get_stoken_obj_id(self, request):
return request.GET.get('stoken', None) return request.GET.get("stoken", None)
def get_stoken_obj(self, request): def get_stoken_obj(self, request):
stoken = self.get_stoken_obj_id(request) stoken = self.get_stoken_obj_id(request)
@ -118,7 +118,7 @@ class BaseViewSet(viewsets.ModelViewSet):
try: try:
return Stoken.objects.get(uid=stoken) return Stoken.objects.get(uid=stoken)
except Stoken.DoesNotExist: except Stoken.DoesNotExist:
raise EtebaseValidationError('bad_stoken', 'Invalid stoken.', status_code=status.HTTP_400_BAD_REQUEST) raise EtebaseValidationError("bad_stoken", "Invalid stoken.", status_code=status.HTTP_400_BAD_REQUEST)
return None return None
@ -127,7 +127,7 @@ class BaseViewSet(viewsets.ModelViewSet):
aggr_fields = [Coalesce(Max(field), V(0)) for field in self.stoken_id_fields] aggr_fields = [Coalesce(Max(field), V(0)) for field in self.stoken_id_fields]
max_stoken = Greatest(*aggr_fields) if len(aggr_fields) > 1 else aggr_fields[0] max_stoken = Greatest(*aggr_fields) if len(aggr_fields) > 1 else aggr_fields[0]
queryset = queryset.annotate(max_stoken=max_stoken).order_by('max_stoken') queryset = queryset.annotate(max_stoken=max_stoken).order_by("max_stoken")
if stoken_rev is not None: if stoken_rev is not None:
queryset = queryset.filter(max_stoken__gt=stoken_rev.id) queryset = queryset.filter(max_stoken__gt=stoken_rev.id)
@ -137,18 +137,18 @@ class BaseViewSet(viewsets.ModelViewSet):
def get_queryset_stoken(self, queryset): def get_queryset_stoken(self, queryset):
maxid = -1 maxid = -1
for row in queryset: for row in queryset:
rowmaxid = getattr(row, 'max_stoken') or -1 rowmaxid = getattr(row, "max_stoken") or -1
maxid = max(maxid, rowmaxid) maxid = max(maxid, rowmaxid)
new_stoken = (maxid >= 0) and Stoken.objects.get(id=maxid) new_stoken = (maxid >= 0) and Stoken.objects.get(id=maxid)
return new_stoken or None return new_stoken or None
def filter_by_stoken_and_limit(self, request, queryset): def filter_by_stoken_and_limit(self, request, queryset):
limit = int(request.GET.get('limit', 50)) limit = int(request.GET.get("limit", 50))
queryset, stoken_rev = self.filter_by_stoken(request, queryset) queryset, stoken_rev = self.filter_by_stoken(request, queryset)
result = list(queryset[:limit + 1]) result = list(queryset[: limit + 1])
if len(result) < limit + 1: if len(result) < limit + 1:
done = True done = True
else: else:
@ -165,21 +165,21 @@ class BaseViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(queryset, many=True) serializer = self.get_serializer(queryset, many=True)
ret = { ret = {
'data': serializer.data, "data": serializer.data,
'done': True, # we always return all the items, so it's always done "done": True, # we always return all the items, so it's always done
} }
return Response(ret) return Response(ret)
class CollectionViewSet(BaseViewSet): class CollectionViewSet(BaseViewSet):
allowed_methods = ['GET', 'POST'] 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 = 'main_item__uid' lookup_field = "main_item__uid"
lookup_url_kwarg = '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):
if queryset is None: if queryset is None:
@ -188,8 +188,8 @@ class CollectionViewSet(BaseViewSet):
def get_serializer_context(self): def get_serializer_context(self):
context = super().get_serializer_context() context = super().get_serializer_context()
prefetch = self.request.query_params.get('prefetch', 'auto') prefetch = self.request.query_params.get("prefetch", "auto")
context.update({'request': self.request, 'prefetch': prefetch}) context.update({"request": self.request, "prefetch": prefetch})
return context return context
def destroy(self, request, uid=None, *args, **kwargs): def destroy(self, request, uid=None, *args, **kwargs):
@ -213,17 +213,18 @@ class CollectionViewSet(BaseViewSet):
queryset = self.get_queryset() queryset = self.get_queryset()
return self.list_common(request, queryset, *args, **kwargs) return self.list_common(request, queryset, *args, **kwargs)
@action_decorator(detail=False, methods=['POST']) @action_decorator(detail=False, methods=["POST"])
def list_multi(self, request, *args, **kwargs): def list_multi(self, request, *args, **kwargs):
serializer = CollectionListMultiSerializer(data=request.data) serializer = CollectionListMultiSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
collection_types = serializer.validated_data['collectionTypes'] collection_types = serializer.validated_data["collectionTypes"]
queryset = self.get_queryset() queryset = self.get_queryset()
# FIXME: Remove the isnull part once we attach collection types to all objects ("collection-type-migration") # FIXME: Remove the isnull part once we attach collection types to all objects ("collection-type-migration")
queryset = queryset.filter( queryset = queryset.filter(
Q(members__collectionType__uid__in=collection_types) | Q(members__collectionType__isnull=True)) Q(members__collectionType__uid__in=collection_types) | Q(members__collectionType__isnull=True)
)
return self.list_common(request, queryset, *args, **kwargs) return self.list_common(request, queryset, *args, **kwargs)
@ -234,51 +235,50 @@ class CollectionViewSet(BaseViewSet):
serializer = self.get_serializer(result, many=True) serializer = self.get_serializer(result, many=True)
ret = { ret = {
'data': serializer.data, "data": serializer.data,
'stoken': new_stoken, "stoken": new_stoken,
'done': done, "done": done,
} }
stoken_obj = self.get_stoken_obj(request) stoken_obj = self.get_stoken_obj(request)
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_qs = CollectionMemberRemoved.objects.filter(user=request.user, stoken__id__gt=stoken_obj.id) remed_qs = CollectionMemberRemoved.objects.filter(user=request.user, stoken__id__gt=stoken_obj.id)
if not ret['done']: if not ret["done"]:
# We only filter by the new_stoken if we are not done. This is because if we are done, the new stoken # We only filter by the new_stoken if we are not done. This is because if we are done, the new stoken
# can point to the most recent collection change rather than most recent removed membership. # 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_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__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]
return Response(ret) return Response(ret)
class CollectionItemViewSet(BaseViewSet): class CollectionItemViewSet(BaseViewSet):
allowed_methods = ['GET', 'POST', 'PUT'] allowed_methods = ["GET", "POST", "PUT"]
permission_classes = BaseViewSet.permission_classes + (permissions.HasWriteAccessOrReadOnly, ) permission_classes = BaseViewSet.permission_classes + (permissions.HasWriteAccessOrReadOnly,)
queryset = CollectionItem.objects.all() queryset = CollectionItem.objects.all()
serializer_class = CollectionItemSerializer serializer_class = CollectionItemSerializer
lookup_field = 'uid' lookup_field = "uid"
stoken_id_fields = ['revisions__stoken__id'] stoken_id_fields = ["revisions__stoken__id"]
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(main_item__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')
queryset = type(self).queryset.filter(collection__pk=collection.pk, queryset = type(self).queryset.filter(collection__pk=collection.pk, revisions__current=True)
revisions__current=True)
return queryset return queryset
def get_serializer_context(self): def get_serializer_context(self):
context = super().get_serializer_context() context = super().get_serializer_context()
prefetch = self.request.query_params.get('prefetch', 'auto') prefetch = self.request.query_params.get("prefetch", "auto")
context.update({'request': self.request, 'prefetch': prefetch}) context.update({"request": self.request, "prefetch": prefetch})
return context return context
def create(self, request, collection_uid=None, *args, **kwargs): def create(self, request, collection_uid=None, *args, **kwargs):
@ -298,7 +298,7 @@ class CollectionItemViewSet(BaseViewSet):
def list(self, request, collection_uid=None, *args, **kwargs): def list(self, request, collection_uid=None, *args, **kwargs):
queryset = self.get_queryset() queryset = self.get_queryset()
if not self.request.query_params.get('withCollection', False): if not self.request.query_params.get("withCollection", False):
queryset = queryset.filter(parent__isnull=True) queryset = queryset.filter(parent__isnull=True)
result, new_stoken_obj, done = self.filter_by_stoken_and_limit(request, queryset) result, new_stoken_obj, done = self.filter_by_stoken_and_limit(request, queryset)
@ -307,27 +307,27 @@ class CollectionItemViewSet(BaseViewSet):
serializer = self.get_serializer(result, many=True) serializer = self.get_serializer(result, many=True)
ret = { ret = {
'data': serializer.data, "data": serializer.data,
'stoken': new_stoken, "stoken": new_stoken,
'done': done, "done": done,
} }
return Response(ret) return Response(ret)
@action_decorator(detail=True, methods=['GET']) @action_decorator(detail=True, methods=["GET"])
def revision(self, request, collection_uid=None, uid=None, *args, **kwargs): 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), main_item__uid=collection_uid)
item = get_object_or_404(col.items, uid=uid) item = get_object_or_404(col.items, uid=uid)
limit = int(request.GET.get('limit', 50)) limit = int(request.GET.get("limit", 50))
iterator = request.GET.get('iterator', None) iterator = request.GET.get("iterator", None)
queryset = item.revisions.order_by('-id') queryset = item.revisions.order_by("-id")
if iterator is not None: if iterator is not None:
iterator = get_object_or_404(queryset, uid=iterator) iterator = get_object_or_404(queryset, uid=iterator)
queryset = queryset.filter(id__lt=iterator.id) queryset = queryset.filter(id__lt=iterator.id)
result = list(queryset[:limit + 1]) result = list(queryset[: limit + 1])
if len(result) < limit + 1: if len(result) < limit + 1:
done = True done = True
else: else:
@ -336,17 +336,17 @@ class CollectionItemViewSet(BaseViewSet):
serializer = CollectionItemRevisionSerializer(result, context=self.get_serializer_context(), many=True) serializer = CollectionItemRevisionSerializer(result, context=self.get_serializer_context(), many=True)
iterator = serializer.data[-1]['uid'] if len(result) > 0 else None iterator = serializer.data[-1]["uid"] if len(result) > 0 else None
ret = { ret = {
'data': serializer.data, "data": serializer.data,
'iterator': iterator, "iterator": iterator,
'done': done, "done": done,
} }
return Response(ret) return Response(ret)
# FIXME: rename to something consistent with what the clients have - maybe list_updates? # FIXME: rename to something consistent with what the clients have - maybe list_updates?
@action_decorator(detail=False, methods=['POST']) @action_decorator(detail=False, methods=["POST"])
def fetch_updates(self, request, collection_uid=None, *args, **kwargs): def fetch_updates(self, request, collection_uid=None, *args, **kwargs):
queryset = self.get_queryset() queryset = self.get_queryset()
@ -356,79 +356,76 @@ class CollectionItemViewSet(BaseViewSet):
item_limit = 200 item_limit = 200
if len(serializer.validated_data) > item_limit: if len(serializer.validated_data) > item_limit:
content = {'code': 'too_many_items', content = {"code": "too_many_items", "detail": "Request has too many items. Limit: {}".format(item_limit)}
'detail': 'Request has too many items. Limit: {}'. format(item_limit)}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)
queryset, stoken_rev = self.filter_by_stoken(request, queryset) queryset, stoken_rev = self.filter_by_stoken(request, queryset)
uids, etags = zip(*[(item['uid'], item.get('etag')) for item in serializer.validated_data]) uids, etags = zip(*[(item["uid"], item.get("etag")) for item in serializer.validated_data])
revs = CollectionItemRevision.objects.filter(uid__in=etags, current=True) revs = CollectionItemRevision.objects.filter(uid__in=etags, current=True)
queryset = queryset.filter(uid__in=uids).exclude(revisions__in=revs) queryset = queryset.filter(uid__in=uids).exclude(revisions__in=revs)
new_stoken_obj = self.get_queryset_stoken(queryset) new_stoken_obj = self.get_queryset_stoken(queryset)
new_stoken = new_stoken_obj and new_stoken_obj.uid new_stoken = new_stoken_obj and new_stoken_obj.uid
stoken = stoken_rev and getattr(stoken_rev, 'uid', None) stoken = stoken_rev and getattr(stoken_rev, "uid", None)
new_stoken = new_stoken or stoken new_stoken = new_stoken or stoken
serializer = self.get_serializer(queryset, many=True) serializer = self.get_serializer(queryset, many=True)
ret = { ret = {
'data': serializer.data, "data": serializer.data,
'stoken': new_stoken, "stoken": new_stoken,
'done': True, # we always return all the items, so it's always done "done": True, # we always return all the items, so it's always done
} }
return Response(ret) return Response(ret)
@action_decorator(detail=False, methods=['POST']) @action_decorator(detail=False, methods=["POST"])
def batch(self, request, collection_uid=None, *args, **kwargs): def batch(self, request, collection_uid=None, *args, **kwargs):
return self.transaction(request, collection_uid, validate_etag=False) return self.transaction(request, collection_uid, validate_etag=False)
@action_decorator(detail=False, methods=['POST']) @action_decorator(detail=False, methods=["POST"])
def transaction(self, request, collection_uid=None, validate_etag=True, *args, **kwargs): def transaction(self, request, collection_uid=None, validate_etag=True, *args, **kwargs):
stoken = request.GET.get('stoken', None) stoken = request.GET.get("stoken", None)
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
main_item__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"}
return Response(content, status=status.HTTP_409_CONFLICT) return Response(content, status=status.HTTP_409_CONFLICT)
items = request.data.get('items') items = request.data.get("items")
deps = request.data.get('deps', None) deps = request.data.get("deps", None)
# FIXME: It should just be one serializer # FIXME: It should just be one serializer
context = self.get_serializer_context() context = self.get_serializer_context()
context.update({'validate_etag': validate_etag}) context.update({"validate_etag": validate_etag})
serializer = self.get_serializer_class()(data=items, context=context, many=True) serializer = self.get_serializer_class()(data=items, context=context, many=True)
deps_serializer = CollectionItemDepSerializer(data=deps, context=context, many=True) deps_serializer = CollectionItemDepSerializer(data=deps, context=context, many=True)
ser_valid = serializer.is_valid() ser_valid = serializer.is_valid()
deps_ser_valid = (deps is None or deps_serializer.is_valid()) deps_ser_valid = deps is None or deps_serializer.is_valid()
if ser_valid and deps_ser_valid: if ser_valid and deps_ser_valid:
items = serializer.save(collection=collection_object) items = serializer.save(collection=collection_object)
ret = { ret = {}
}
return Response(ret, status=status.HTTP_200_OK) return Response(ret, status=status.HTTP_200_OK)
return Response( return Response(
{ {"items": serializer.errors, "deps": deps_serializer.errors if deps is not None else [],},
"items": serializer.errors, status=status.HTTP_409_CONFLICT,
"deps": deps_serializer.errors if deps is not None else [], )
},
status=status.HTTP_409_CONFLICT)
class CollectionItemChunkViewSet(viewsets.ViewSet): class CollectionItemChunkViewSet(viewsets.ViewSet):
allowed_methods = ['GET', 'PUT'] allowed_methods = ["GET", "PUT"]
authentication_classes = BaseViewSet.authentication_classes authentication_classes = BaseViewSet.authentication_classes
permission_classes = BaseViewSet.permission_classes permission_classes = BaseViewSet.permission_classes
renderer_classes = BaseViewSet.renderer_classes renderer_classes = BaseViewSet.renderer_classes
parser_classes = (ChunkUploadParser, ) parser_classes = (ChunkUploadParser,)
serializer_class = CollectionItemChunkSerializer serializer_class = CollectionItemChunkSerializer
lookup_field = 'uid' lookup_field = "uid"
def get_serializer_class(self): def get_serializer_class(self):
return self.serializer_class return self.serializer_class
@ -452,13 +449,12 @@ class CollectionItemChunkViewSet(viewsets.ViewSet):
serializer.save(collection=col) serializer.save(collection=col)
except IntegrityError: except IntegrityError:
return Response( return Response(
{"code": "chunk_exists", "detail": "Chunk already exists."}, {"code": "chunk_exists", "detail": "Chunk already exists."}, status=status.HTTP_409_CONFLICT
status=status.HTTP_409_CONFLICT
) )
return Response({}, status=status.HTTP_201_CREATED) return Response({}, status=status.HTTP_201_CREATED)
@action_decorator(detail=True, methods=['GET']) @action_decorator(detail=True, methods=["GET"])
def download(self, request, collection_uid=None, collection_item_uid=None, uid=None, *args, **kwargs): def download(self, request, collection_uid=None, collection_item_uid=None, uid=None, *args, **kwargs):
import os import os
from django.views.static import serve from django.views.static import serve
@ -476,24 +472,24 @@ class CollectionItemChunkViewSet(viewsets.ViewSet):
class CollectionMemberViewSet(BaseViewSet): class CollectionMemberViewSet(BaseViewSet):
allowed_methods = ['GET', 'PUT', 'DELETE'] allowed_methods = ["GET", "PUT", "DELETE"]
our_base_permission_classes = BaseViewSet.permission_classes our_base_permission_classes = BaseViewSet.permission_classes
permission_classes = our_base_permission_classes + (permissions.IsCollectionAdmin, ) permission_classes = our_base_permission_classes + (permissions.IsCollectionAdmin,)
queryset = CollectionMember.objects.all() queryset = CollectionMember.objects.all()
serializer_class = CollectionMemberSerializer serializer_class = CollectionMemberSerializer
lookup_field = f'user__{User.USERNAME_FIELD}__iexact' lookup_field = f"user__{User.USERNAME_FIELD}__iexact"
lookup_url_kwarg = 'username' lookup_url_kwarg = "username"
stoken_id_fields = ['stoken__id'] stoken_id_fields = ["stoken__id"]
# FIXME: need to make sure that there's always an admin, and maybe also don't let an owner remove adm access # FIXME: need to make sure that there's always an admin, and maybe also don't let an owner remove adm access
# (if we want to transfer, we need to do that specifically) # (if we want to transfer, we need to do that specifically)
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(main_item__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")
if queryset is None: if queryset is None:
queryset = type(self).queryset queryset = type(self).queryset
@ -502,18 +498,18 @@ class CollectionMemberViewSet(BaseViewSet):
# We override this method because we expect the stoken to be called iterator # We override this method because we expect the stoken to be called iterator
def get_stoken_obj_id(self, request): def get_stoken_obj_id(self, request):
return request.GET.get('iterator', None) return request.GET.get("iterator", None)
def list(self, request, collection_uid=None, *args, **kwargs): def list(self, request, collection_uid=None, *args, **kwargs):
queryset = self.get_queryset().order_by('id') queryset = self.get_queryset().order_by("id")
result, new_stoken_obj, done = self.filter_by_stoken_and_limit(request, queryset) result, new_stoken_obj, done = self.filter_by_stoken_and_limit(request, queryset)
new_stoken = new_stoken_obj and new_stoken_obj.uid new_stoken = new_stoken_obj and new_stoken_obj.uid
serializer = self.get_serializer(result, many=True) serializer = self.get_serializer(result, many=True)
ret = { ret = {
'data': serializer.data, "data": serializer.data,
'iterator': new_stoken, # Here we call it an iterator, it's only stoken for collection/items "iterator": new_stoken, # Here we call it an iterator, it's only stoken for collection/items
'done': done, "done": done,
} }
return Response(ret) return Response(ret)
@ -526,9 +522,9 @@ class CollectionMemberViewSet(BaseViewSet):
def perform_destroy(self, instance): def perform_destroy(self, instance):
instance.revoke() instance.revoke()
@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, *args, **kwargs): def leave(self, request, collection_uid=None, *args, **kwargs):
collection_uid = self.kwargs['collection_uid'] 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), main_item__uid=collection_uid)
member = col.members.get(user=request.user) member = col.members.get(user=request.user)
@ -540,20 +536,20 @@ class CollectionMemberViewSet(BaseViewSet):
class InvitationBaseViewSet(BaseViewSet): class InvitationBaseViewSet(BaseViewSet):
queryset = CollectionInvitation.objects.all() queryset = CollectionInvitation.objects.all()
serializer_class = CollectionInvitationSerializer serializer_class = CollectionInvitationSerializer
lookup_field = 'uid' lookup_field = "uid"
lookup_url_kwarg = 'invitation_uid' lookup_url_kwarg = "invitation_uid"
def list(self, request, collection_uid=None, *args, **kwargs): def list(self, request, collection_uid=None, *args, **kwargs):
limit = int(request.GET.get('limit', 50)) limit = int(request.GET.get("limit", 50))
iterator = request.GET.get('iterator', None) iterator = request.GET.get("iterator", None)
queryset = self.get_queryset().order_by('id') queryset = self.get_queryset().order_by("id")
if iterator is not None: if iterator is not None:
iterator = get_object_or_404(queryset, uid=iterator) iterator = get_object_or_404(queryset, uid=iterator)
queryset = queryset.filter(id__gt=iterator.id) queryset = queryset.filter(id__gt=iterator.id)
result = list(queryset[:limit + 1]) result = list(queryset[: limit + 1])
if len(result) < limit + 1: if len(result) < limit + 1:
done = True done = True
else: else:
@ -562,19 +558,19 @@ class InvitationBaseViewSet(BaseViewSet):
serializer = self.get_serializer(result, many=True) serializer = self.get_serializer(result, many=True)
iterator = serializer.data[-1]['uid'] if len(result) > 0 else None iterator = serializer.data[-1]["uid"] if len(result) > 0 else None
ret = { ret = {
'data': serializer.data, "data": serializer.data,
'iterator': iterator, "iterator": iterator,
'done': done, "done": done,
} }
return Response(ret) return Response(ret)
class InvitationOutgoingViewSet(InvitationBaseViewSet): class InvitationOutgoingViewSet(InvitationBaseViewSet):
allowed_methods = ['GET', 'POST', 'PUT', 'DELETE'] allowed_methods = ["GET", "POST", "PUT", "DELETE"]
def get_queryset(self, queryset=None): def get_queryset(self, queryset=None):
if queryset is None: if queryset is None:
@ -585,28 +581,29 @@ class InvitationOutgoingViewSet(InvitationBaseViewSet):
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)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
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(main_item__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")
if request.user == serializer.validated_data.get('user'): if request.user == serializer.validated_data.get("user"):
content = {'code': 'self_invite', 'detail': 'Inviting yourself is invalid'} content = {"code": "self_invite", "detail": "Inviting yourself is invalid"}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)
if not permissions.is_collection_admin(collection, request.user): if not permissions.is_collection_admin(collection, request.user):
raise PermissionDenied({'code': 'admin_access_required', raise PermissionDenied(
'detail': 'User is not an admin of this collection'}) {"code": "admin_access_required", "detail": "User is not an admin of this collection"}
)
serializer.save(collection=collection) serializer.save(collection=collection)
return Response({}, status=status.HTTP_201_CREATED) return Response({}, status=status.HTTP_201_CREATED)
@action_decorator(detail=False, allowed_methods=['GET'], methods=['GET']) @action_decorator(detail=False, allowed_methods=["GET"], methods=["GET"])
def fetch_user_profile(self, request, *args, **kwargs): def fetch_user_profile(self, request, *args, **kwargs):
username = request.GET.get('username') username = request.GET.get("username")
kwargs = {User.USERNAME_FIELD: username.lower()} kwargs = {User.USERNAME_FIELD: username.lower()}
user = get_object_or_404(get_user_queryset(User.objects.all(), self), **kwargs) user = get_object_or_404(get_user_queryset(User.objects.all(), self), **kwargs)
user_info = get_object_or_404(UserInfo.objects.all(), owner=user) user_info = get_object_or_404(UserInfo.objects.all(), owner=user)
@ -615,7 +612,7 @@ class InvitationOutgoingViewSet(InvitationBaseViewSet):
class InvitationIncomingViewSet(InvitationBaseViewSet): class InvitationIncomingViewSet(InvitationBaseViewSet):
allowed_methods = ['GET', 'DELETE'] allowed_methods = ["GET", "DELETE"]
def get_queryset(self, queryset=None): def get_queryset(self, queryset=None):
if queryset is None: if queryset is None:
@ -623,11 +620,11 @@ class InvitationIncomingViewSet(InvitationBaseViewSet):
return queryset.filter(user=self.request.user) return queryset.filter(user=self.request.user)
@action_decorator(detail=True, allowed_methods=['POST'], methods=['POST']) @action_decorator(detail=True, allowed_methods=["POST"], methods=["POST"])
def accept(self, request, invitation_uid=None, *args, **kwargs): def accept(self, request, invitation_uid=None, *args, **kwargs):
invitation = get_object_or_404(self.get_queryset(), uid=invitation_uid) invitation = get_object_or_404(self.get_queryset(), uid=invitation_uid)
context = self.get_serializer_context() context = self.get_serializer_context()
context.update({'invitation': invitation}) context.update({"invitation": invitation})
serializer = InvitationAcceptSerializer(data=request.data, context=context) serializer = InvitationAcceptSerializer(data=request.data, context=context)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -636,36 +633,37 @@ class InvitationIncomingViewSet(InvitationBaseViewSet):
class AuthenticationViewSet(viewsets.ViewSet): class AuthenticationViewSet(viewsets.ViewSet):
allowed_methods = ['POST'] allowed_methods = ["POST"]
authentication_classes = BaseViewSet.authentication_classes authentication_classes = BaseViewSet.authentication_classes
renderer_classes = BaseViewSet.renderer_classes renderer_classes = BaseViewSet.renderer_classes
parser_classes = BaseViewSet.parser_classes parser_classes = BaseViewSet.parser_classes
def get_encryption_key(self, salt): def get_encryption_key(self, salt):
key = nacl.hash.blake2b(settings.SECRET_KEY.encode(), encoder=nacl.encoding.RawEncoder) key = nacl.hash.blake2b(settings.SECRET_KEY.encode(), encoder=nacl.encoding.RawEncoder)
return nacl.hash.blake2b(b'', key=key, salt=salt[:nacl.hash.BLAKE2B_SALTBYTES], person=b'etebase-auth', return nacl.hash.blake2b(
encoder=nacl.encoding.RawEncoder) b"",
key=key,
salt=salt[: nacl.hash.BLAKE2B_SALTBYTES],
person=b"etebase-auth",
encoder=nacl.encoding.RawEncoder,
)
def get_queryset(self): def get_queryset(self):
return get_user_queryset(User.objects.all(), self) return get_user_queryset(User.objects.all(), self)
def get_serializer_context(self): def get_serializer_context(self):
return { return {"request": self.request, "format": self.format_kwarg, "view": self}
'request': self.request,
'format': self.format_kwarg,
'view': self
}
def login_response_data(self, user): def login_response_data(self, user):
return { return {
'token': AuthToken.objects.create(user=user).key, "token": AuthToken.objects.create(user=user).key,
'user': UserSerializer(user).data, "user": UserSerializer(user).data,
} }
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
@action_decorator(detail=False, methods=['POST']) @action_decorator(detail=False, methods=["POST"])
def signup(self, request, *args, **kwargs): def signup(self, request, *args, **kwargs):
serializer = AuthenticationSignupSerializer(data=request.data, context=self.get_serializer_context()) serializer = AuthenticationSignupSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -677,23 +675,23 @@ class AuthenticationViewSet(viewsets.ViewSet):
return Response(data, status=status.HTTP_201_CREATED) return Response(data, status=status.HTTP_201_CREATED)
def get_login_user(self, username): def get_login_user(self, username):
kwargs = {User.USERNAME_FIELD + '__iexact': username.lower()} kwargs = {User.USERNAME_FIELD + "__iexact": username.lower()}
try: try:
user = self.get_queryset().get(**kwargs) user = self.get_queryset().get(**kwargs)
if not hasattr(user, 'userinfo'): if not hasattr(user, "userinfo"):
raise AuthenticationFailed({'code': 'user_not_init', 'detail': 'User not properly init'}) raise AuthenticationFailed({"code": "user_not_init", "detail": "User not properly init"})
return user return user
except User.DoesNotExist: except User.DoesNotExist:
raise AuthenticationFailed({'code': 'user_not_found', 'detail': 'User not found'}) raise AuthenticationFailed({"code": "user_not_found", "detail": "User not found"})
def validate_login_request(self, request, validated_data, response_raw, signature, expected_action): def validate_login_request(self, request, validated_data, response_raw, signature, expected_action):
from datetime import datetime from datetime import datetime
username = validated_data.get('username') username = validated_data.get("username")
user = self.get_login_user(username) user = self.get_login_user(username)
host = validated_data['host'] host = validated_data["host"]
challenge = validated_data['challenge'] challenge = validated_data["challenge"]
action = validated_data['action'] action = validated_data["action"]
salt = bytes(user.userinfo.salt) salt = bytes(user.userinfo.salt)
enc_key = self.get_encryption_key(salt) enc_key = self.get_encryption_key(salt)
@ -702,17 +700,17 @@ class AuthenticationViewSet(viewsets.ViewSet):
challenge_data = msgpack_decode(box.decrypt(challenge)) challenge_data = msgpack_decode(box.decrypt(challenge))
now = int(datetime.now().timestamp()) now = int(datetime.now().timestamp())
if action != expected_action: if action != expected_action:
content = {'code': 'wrong_action', 'detail': 'Expected "{}" but got something else'.format(expected_action)} content = {"code": "wrong_action", "detail": 'Expected "{}" but got something else'.format(expected_action)}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif now - challenge_data['timestamp'] > app_settings.CHALLENGE_VALID_SECONDS: elif now - challenge_data["timestamp"] > app_settings.CHALLENGE_VALID_SECONDS:
content = {'code': 'challenge_expired', 'detail': 'Login challange has expired'} content = {"code": "challenge_expired", "detail": "Login challange has expired"}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif challenge_data['userId'] != user.id: elif challenge_data["userId"] != user.id:
content = {'code': 'wrong_user', 'detail': 'This challenge is for the wrong user'} content = {"code": "wrong_user", "detail": "This challenge is for the wrong user"}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif not settings.DEBUG and host.split(':', 1)[0] != request.get_host(): elif not settings.DEBUG and host.split(":", 1)[0] != request.get_host():
detail = 'Found wrong host name. Got: "{}" expected: "{}"'.format(host, request.get_host()) detail = 'Found wrong host name. Got: "{}" expected: "{}"'.format(host, request.get_host())
content = {'code': 'wrong_host', 'detail': detail} content = {"code": "wrong_host", "detail": detail}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)
verify_key = nacl.signing.VerifyKey(bytes(user.userinfo.loginPubkey), encoder=nacl.encoding.RawEncoder) verify_key = nacl.signing.VerifyKey(bytes(user.userinfo.loginPubkey), encoder=nacl.encoding.RawEncoder)
@ -720,22 +718,24 @@ class AuthenticationViewSet(viewsets.ViewSet):
try: try:
verify_key.verify(response_raw, signature) verify_key.verify(response_raw, signature)
except nacl.exceptions.BadSignatureError: except nacl.exceptions.BadSignatureError:
return Response({'code': 'login_bad_signature', 'detail': 'Wrong password for user.'}, return Response(
status=status.HTTP_401_UNAUTHORIZED) {"code": "login_bad_signature", "detail": "Wrong password for user."},
status=status.HTTP_401_UNAUTHORIZED,
)
return None return None
@action_decorator(detail=False, methods=['GET']) @action_decorator(detail=False, methods=["GET"])
def is_etebase(self, request, *args, **kwargs): def is_etebase(self, request, *args, **kwargs):
return Response({}, status=status.HTTP_200_OK) return Response({}, status=status.HTTP_200_OK)
@action_decorator(detail=False, methods=['POST']) @action_decorator(detail=False, methods=["POST"])
def login_challenge(self, request, *args, **kwargs): def login_challenge(self, request, *args, **kwargs):
from datetime import datetime from datetime import datetime
serializer = AuthenticationLoginChallengeSerializer(data=request.data) serializer = AuthenticationLoginChallengeSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
username = serializer.validated_data.get('username') username = serializer.validated_data.get("username")
user = self.get_login_user(username) user = self.get_login_user(username)
salt = bytes(user.userinfo.salt) salt = bytes(user.userinfo.salt)
@ -755,25 +755,26 @@ class AuthenticationViewSet(viewsets.ViewSet):
} }
return Response(ret, status=status.HTTP_200_OK) return Response(ret, status=status.HTTP_200_OK)
@action_decorator(detail=False, methods=['POST']) @action_decorator(detail=False, methods=["POST"])
def login(self, request, *args, **kwargs): def login(self, request, *args, **kwargs):
outer_serializer = AuthenticationLoginSerializer(data=request.data) outer_serializer = AuthenticationLoginSerializer(data=request.data)
outer_serializer.is_valid(raise_exception=True) outer_serializer.is_valid(raise_exception=True)
response_raw = outer_serializer.validated_data['response'] response_raw = outer_serializer.validated_data["response"]
response = msgpack_decode(response_raw) response = msgpack_decode(response_raw)
signature = outer_serializer.validated_data['signature'] signature = outer_serializer.validated_data["signature"]
context = {'host': request.get_host()} context = {"host": request.get_host()}
serializer = AuthenticationLoginInnerSerializer(data=response, context=context) serializer = AuthenticationLoginInnerSerializer(data=response, context=context)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
bad_login_response = self.validate_login_request( bad_login_response = self.validate_login_request(
request, serializer.validated_data, response_raw, signature, "login") request, serializer.validated_data, response_raw, signature, "login"
)
if bad_login_response is not None: if bad_login_response is not None:
return bad_login_response return bad_login_response
username = serializer.validated_data.get('username') username = serializer.validated_data.get("username")
user = self.get_login_user(username) user = self.get_login_user(username)
data = self.login_response_data(user) data = self.login_response_data(user)
@ -782,27 +783,28 @@ class AuthenticationViewSet(viewsets.ViewSet):
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
@action_decorator(detail=False, methods=['POST'], permission_classes=[IsAuthenticated]) @action_decorator(detail=False, methods=["POST"], permission_classes=[IsAuthenticated])
def logout(self, request, *args, **kwargs): def logout(self, request, *args, **kwargs):
request.auth.delete() request.auth.delete()
user_logged_out.send(sender=request.user.__class__, request=request, user=request.user) user_logged_out.send(sender=request.user.__class__, request=request, user=request.user)
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@action_decorator(detail=False, methods=['POST'], permission_classes=BaseViewSet.permission_classes) @action_decorator(detail=False, methods=["POST"], permission_classes=BaseViewSet.permission_classes)
def change_password(self, request, *args, **kwargs): def change_password(self, request, *args, **kwargs):
outer_serializer = AuthenticationLoginSerializer(data=request.data) outer_serializer = AuthenticationLoginSerializer(data=request.data)
outer_serializer.is_valid(raise_exception=True) outer_serializer.is_valid(raise_exception=True)
response_raw = outer_serializer.validated_data['response'] response_raw = outer_serializer.validated_data["response"]
response = msgpack_decode(response_raw) response = msgpack_decode(response_raw)
signature = outer_serializer.validated_data['signature'] signature = outer_serializer.validated_data["signature"]
context = {'host': request.get_host()} context = {"host": request.get_host()}
serializer = AuthenticationChangePasswordInnerSerializer(request.user.userinfo, data=response, context=context) serializer = AuthenticationChangePasswordInnerSerializer(request.user.userinfo, data=response, context=context)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
bad_login_response = self.validate_login_request( bad_login_response = self.validate_login_request(
request, serializer.validated_data, response_raw, signature, "changePassword") request, serializer.validated_data, response_raw, signature, "changePassword"
)
if bad_login_response is not None: if bad_login_response is not None:
return bad_login_response return bad_login_response
@ -810,35 +812,32 @@ class AuthenticationViewSet(viewsets.ViewSet):
return Response({}, status=status.HTTP_200_OK) return Response({}, status=status.HTTP_200_OK)
@action_decorator(detail=False, methods=['POST'], permission_classes=[IsAuthenticated]) @action_decorator(detail=False, methods=["POST"], permission_classes=[IsAuthenticated])
def dashboard_url(self, request, *args, **kwargs): def dashboard_url(self, request, *args, **kwargs):
get_dashboard_url = app_settings.DASHBOARD_URL_FUNC get_dashboard_url = app_settings.DASHBOARD_URL_FUNC
if get_dashboard_url is None: if get_dashboard_url is None:
raise EtebaseValidationError('not_supported', 'This server doesn\'t have a user dashboard.', raise EtebaseValidationError(
status_code=status.HTTP_400_BAD_REQUEST) "not_supported", "This server doesn't have a user dashboard.", status_code=status.HTTP_400_BAD_REQUEST
)
ret = { ret = {
'url': get_dashboard_url(request, *args, **kwargs), "url": get_dashboard_url(request, *args, **kwargs),
} }
return Response(ret) return Response(ret)
class TestAuthenticationViewSet(viewsets.ViewSet): class TestAuthenticationViewSet(viewsets.ViewSet):
allowed_methods = ['POST'] allowed_methods = ["POST"]
renderer_classes = BaseViewSet.renderer_classes renderer_classes = BaseViewSet.renderer_classes
parser_classes = BaseViewSet.parser_classes parser_classes = BaseViewSet.parser_classes
def get_serializer_context(self): def get_serializer_context(self):
return { return {"request": self.request, "format": self.format_kwarg, "view": self}
'request': self.request,
'format': self.format_kwarg,
'view': self
}
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
@action_decorator(detail=False, methods=['POST']) @action_decorator(detail=False, methods=["POST"])
def reset(self, request, *args, **kwargs): def reset(self, request, *args, **kwargs):
# Only run when in DEBUG mode! It's only used for tests # Only run when in DEBUG mode! It's only used for tests
if not settings.DEBUG: if not settings.DEBUG:
@ -846,13 +845,13 @@ class TestAuthenticationViewSet(viewsets.ViewSet):
with transaction.atomic(): with transaction.atomic():
user_queryset = get_user_queryset(User.objects.all(), self) user_queryset = get_user_queryset(User.objects.all(), self)
user = get_object_or_404(user_queryset, username=request.data.get('user').get('username')) user = get_object_or_404(user_queryset, username=request.data.get("user").get("username"))
# Only allow test users for extra safety # Only allow test users for extra safety
if not getattr(user, User.USERNAME_FIELD).startswith('test_user'): if not getattr(user, User.USERNAME_FIELD).startswith("test_user"):
return HttpResponseBadRequest("Endpoint not allowed for user.") return HttpResponseBadRequest("Endpoint not allowed for user.")
if hasattr(user, 'userinfo'): if hasattr(user, "userinfo"):
user.userinfo.delete() user.userinfo.delete()
serializer = AuthenticationSignupSerializer(data=request.data, context=self.get_serializer_context()) serializer = AuthenticationSignupSerializer(data=request.data, context=self.get_serializer_context())

View File

@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'etebase_server.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "etebase_server.settings")
application = get_asgi_application() application = get_asgi_application()

View File

@ -17,7 +17,7 @@ from .utils import get_secret_from_file
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
AUTH_USER_MODEL = 'myauth.User' AUTH_USER_MODEL = "myauth.User"
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
@ -37,10 +37,9 @@ ALLOWED_HOSTS = []
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases # https://docs.djangoproject.com/en/2.0/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.sqlite3', "ENGINE": "django.db.backends.sqlite3",
'NAME': os.environ.get('ETEBASE_DB_PATH', "NAME": os.environ.get("ETEBASE_DB_PATH", os.path.join(BASE_DIR, "db.sqlite3")),
os.path.join(BASE_DIR, 'db.sqlite3')),
} }
} }
@ -48,78 +47,68 @@ DATABASES = {
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', "django.contrib.admin",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.staticfiles', "django.contrib.staticfiles",
'corsheaders', "corsheaders",
'rest_framework', "rest_framework",
'myauth.apps.MyauthConfig', "myauth.apps.MyauthConfig",
'django_etebase.apps.DjangoEtebaseConfig', "django_etebase.apps.DjangoEtebaseConfig",
'django_etebase.token_auth.apps.TokenAuthConfig', "django_etebase.token_auth.apps.TokenAuthConfig",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'corsheaders.middleware.CorsMiddleware', "corsheaders.middleware.CorsMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
ROOT_URLCONF = 'etebase_server.urls' ROOT_URLCONF = "etebase_server.urls"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [ "DIRS": [os.path.join(BASE_DIR, "templates")],
os.path.join(BASE_DIR, 'templates') "APP_DIRS": True,
], "OPTIONS": {
'APP_DIRS': True, "context_processors": [
'OPTIONS': { "django.template.context_processors.debug",
'context_processors': [ "django.template.context_processors.request",
'django.template.context_processors.debug', "django.contrib.auth.context_processors.auth",
'django.template.context_processors.request', "django.contrib.messages.context_processors.messages",
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
], ],
}, },
}, },
] ]
WSGI_APPLICATION = 'etebase_server.wsgi.application' WSGI_APPLICATION = "etebase_server.wsgi.application"
# Password validation # Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",},
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},
}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},
{ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
] ]
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/ # https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC' TIME_ZONE = "UTC"
USE_I18N = True USE_I18N = True
@ -133,18 +122,18 @@ CORS_ORIGIN_ALLOW_ALL = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/ # https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = "/static/"
STATIC_ROOT = os.environ.get('DJANGO_STATIC_ROOT', os.path.join(BASE_DIR, 'static')) STATIC_ROOT = os.environ.get("DJANGO_STATIC_ROOT", os.path.join(BASE_DIR, "static"))
MEDIA_ROOT = os.environ.get('DJANGO_MEDIA_ROOT', os.path.join(BASE_DIR, 'media')) MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT", os.path.join(BASE_DIR, "media"))
MEDIA_URL = '/user-media/' MEDIA_URL = "/user-media/"
# Define where to find configuration files # Define where to find configuration files
config_locations = [ config_locations = [
os.environ.get('ETEBASE_EASY_CONFIG_PATH', ''), os.environ.get("ETEBASE_EASY_CONFIG_PATH", ""),
'etebase-server.ini', "etebase-server.ini",
'/etc/etebase-server/etebase-server.ini', "/etc/etebase-server/etebase-server.ini",
] ]
# Use config file if present # Use config file if present
@ -152,27 +141,29 @@ if any(os.path.isfile(x) for x in config_locations):
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read(config_locations) config.read(config_locations)
section = config['global'] section = config["global"]
SECRET_FILE = section.get('secret_file', SECRET_FILE) SECRET_FILE = section.get("secret_file", SECRET_FILE)
STATIC_ROOT = section.get('static_root', STATIC_ROOT) STATIC_ROOT = section.get("static_root", STATIC_ROOT)
STATIC_URL = section.get('static_url', STATIC_URL) STATIC_URL = section.get("static_url", STATIC_URL)
MEDIA_ROOT = section.get('media_root', MEDIA_ROOT) MEDIA_ROOT = section.get("media_root", MEDIA_ROOT)
MEDIA_URL = section.get('media_url', MEDIA_URL) MEDIA_URL = section.get("media_url", MEDIA_URL)
LANGUAGE_CODE = section.get('language_code', LANGUAGE_CODE) LANGUAGE_CODE = section.get("language_code", LANGUAGE_CODE)
TIME_ZONE = section.get('time_zone', TIME_ZONE) TIME_ZONE = section.get("time_zone", TIME_ZONE)
DEBUG = section.getboolean('debug', DEBUG) DEBUG = section.getboolean("debug", DEBUG)
if 'allowed_hosts' in config: if "allowed_hosts" in config:
ALLOWED_HOSTS = [y for x, y in config.items('allowed_hosts')] ALLOWED_HOSTS = [y for x, y in config.items("allowed_hosts")]
if 'database' in config: if "database" in config:
DATABASES = { 'default': { x.upper(): y for x, y in config.items('database') } } DATABASES = {"default": {x.upper(): y for x, y in config.items("database")}}
ETEBASE_API_PERMISSIONS = ('rest_framework.permissions.IsAuthenticated', ) ETEBASE_API_PERMISSIONS = ("rest_framework.permissions.IsAuthenticated",)
ETEBASE_API_AUTHENTICATORS = ('django_etebase.token_auth.authentication.TokenAuthentication', ETEBASE_API_AUTHENTICATORS = (
'rest_framework.authentication.SessionAuthentication') "django_etebase.token_auth.authentication.TokenAuthentication",
ETEBASE_CREATE_USER_FUNC = 'django_etebase.utils.create_user_blocked' "rest_framework.authentication.SessionAuthentication",
)
ETEBASE_CREATE_USER_FUNC = "django_etebase.utils.create_user_blocked"
# Make an `etebase_server_settings` module available to override settings. # Make an `etebase_server_settings` module available to override settings.
try: try:
@ -180,5 +171,5 @@ try:
except ImportError: except ImportError:
pass pass
if 'SECRET_KEY' not in locals(): if "SECRET_KEY" not in locals():
SECRET_KEY = get_secret_from_file(SECRET_FILE) SECRET_KEY = get_secret_from_file(SECRET_FILE)

View File

@ -5,13 +5,12 @@ from django.urls import path
from django.views.generic import TemplateView from django.views.generic import TemplateView
urlpatterns = [ urlpatterns = [
url(r'^api/', include('django_etebase.urls')), url(r"^api/", include("django_etebase.urls")),
url(r'^admin/', admin.site.urls), url(r"^admin/", admin.site.urls),
path("", TemplateView.as_view(template_name="success.html")),
path('', TemplateView.as_view(template_name='success.html')),
] ]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += [ urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), url(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework")),
] ]

View File

@ -14,6 +14,7 @@
from django.core.management import utils from django.core.management import utils
def get_secret_from_file(path): def get_secret_from_file(path):
try: try:
with open(path, "r") as f: with open(path, "r") as f:

View File

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'etebase_server.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "etebase_server.settings")
application = get_wsgi_application() application = get_wsgi_application()

View File

@ -5,7 +5,7 @@ import sys
def main(): def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'etebase_server.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "etebase_server.settings")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
@ -17,5 +17,5 @@ def main():
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -6,11 +6,7 @@ from .forms import AdminUserCreationForm
class UserAdmin(DjangoUserAdmin): class UserAdmin(DjangoUserAdmin):
add_form = AdminUserCreationForm add_form = AdminUserCreationForm
add_fieldsets = ( add_fieldsets = ((None, {"classes": ("wide",), "fields": ("username",),}),)
(None, {
'classes': ('wide',),
'fields': ('username', ),
}),
)
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)

View File

@ -2,4 +2,4 @@ from django.apps import AppConfig
class MyauthConfig(AppConfig): class MyauthConfig(AppConfig):
name = 'myauth' name = "myauth"

View File

@ -14,12 +14,12 @@ class AdminUserCreationForm(forms.ModelForm):
class Meta: class Meta:
model = User model = User
fields = ("username",) fields = ("username",)
field_classes = {'username': UsernameField} field_classes = {"username": UsernameField}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self._meta.model.USERNAME_FIELD in self.fields: if self._meta.model.USERNAME_FIELD in self.fields:
self.fields[self._meta.model.USERNAME_FIELD].widget.attrs['autofocus'] = True self.fields[self._meta.model.USERNAME_FIELD].widget.attrs["autofocus"] = True
def save(self, commit=True): def save(self, commit=True):
user = super().save(commit=False) user = super().save(commit=False)
@ -27,4 +27,3 @@ class AdminUserCreationForm(forms.ModelForm):
if commit: if commit:
user.save() user.save()
return user return user

View File

@ -11,34 +11,79 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('auth', '0011_update_proxy_permissions'), ("auth", "0011_update_proxy_permissions"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='User', name="User",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('password', models.CharField(max_length=128, verbose_name='password')), ("password", models.CharField(max_length=128, verbose_name="password")),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ("last_login", models.DateTimeField(blank=True, null=True, verbose_name="last login")),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), (
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), "is_superuser",
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), models.BooleanField(
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), default=False,
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), help_text="Designates that this user has all permissions without explicitly assigning them.",
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), verbose_name="superuser status",
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), (
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), "username",
], models.CharField(
options={ error_messages={"unique": "A user with that username already exists."},
'verbose_name': 'user', help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
'verbose_name_plural': 'users', max_length=150,
'abstract': False, unique=True,
}, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
managers=[ verbose_name="username",
('objects', django.contrib.auth.models.UserManager()), ),
),
("first_name", models.CharField(blank=True, max_length=30, verbose_name="first name")),
("last_name", models.CharField(blank=True, max_length=150, verbose_name="last name")),
("email", models.EmailField(blank=True, max_length=254, verbose_name="email address")),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
("date_joined", models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined")),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.Group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.Permission",
verbose_name="user permissions",
),
),
], ],
options={"verbose_name": "user", "verbose_name_plural": "users", "abstract": False,},
managers=[("objects", django.contrib.auth.models.UserManager()),],
), ),
] ]

View File

@ -7,13 +7,20 @@ import myauth.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('myauth', '0001_initial'), ("myauth", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='user', model_name="user",
name='username', name="username",
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and ./+/-/_ only.', max_length=150, unique=True, validators=[myauth.models.UnicodeUsernameValidator()], verbose_name='username'), field=models.CharField(
error_messages={"unique": "A user with that username already exists."},
help_text="Required. 150 characters or fewer. Letters, digits and ./+/-/_ only.",
max_length=150,
unique=True,
validators=[myauth.models.UnicodeUsernameValidator()],
verbose_name="username",
),
), ),
] ]

View File

@ -7,17 +7,14 @@ from django.utils.translation import gettext_lazy as _
@deconstructible @deconstructible
class UnicodeUsernameValidator(validators.RegexValidator): class UnicodeUsernameValidator(validators.RegexValidator):
regex = r'^[\w.-]+\Z' regex = r"^[\w.-]+\Z"
message = _( message = _("Enter a valid username. This value may contain only letters, " "numbers, and ./-/_ characters.")
'Enter a valid username. This value may contain only letters, '
'numbers, and ./-/_ characters.'
)
flags = 0 flags = 0
class UserManager(DjangoUserManager): class UserManager(DjangoUserManager):
def get_by_natural_key(self, username): def get_by_natural_key(self, username):
return self.get(**{self.model.USERNAME_FIELD + '__iexact': username}) return self.get(**{self.model.USERNAME_FIELD + "__iexact": username})
class User(AbstractUser): class User(AbstractUser):
@ -26,14 +23,12 @@ class User(AbstractUser):
objects = UserManager() objects = UserManager()
username = models.CharField( username = models.CharField(
_('username'), _("username"),
max_length=150, max_length=150,
unique=True, unique=True,
help_text=_('Required. 150 characters or fewer. Letters, digits and ./-/_ only.'), help_text=_("Required. 150 characters or fewer. Letters, digits and ./-/_ only."),
validators=[username_validator], validators=[username_validator],
error_messages={ error_messages={"unique": _("A user with that username already exists."),},
'unique': _("A user with that username already exists."),
},
) )
@classmethod @classmethod

2
pyproject.toml Normal file
View File

@ -0,0 +1,2 @@
[tool.black]
line-length = 120

View File

@ -1,3 +1,4 @@
coverage coverage
pip-tools pip-tools
pywatchman pywatchman
black