diff --git a/ChangeLog.md b/ChangeLog.md
index 8845089..d3adef0 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,10 @@
# Changelog
+## Version 0.5.2
+* Fix issues with host verification failing with a custom port
+* Add env variable to change configuration file path.
+* Change user creation to not ask for a password (and clarify the readme).
+
## Version 0.5.1
* Enforce collections to always have a collection type set
* Collection saving: add another verification for collection UID uniqueness.
diff --git a/README.md b/README.md
index 0ecf52a..dc05b31 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,9 @@
Etebase - Encrypt Everything
-A skeleton app for running your own [Etebase](https://www.etebase.com) (EteSync 2.0) server.
+An [Etebase](https://www.etebase.com) (EteSync 2.0) server so you can run your own.
+
+[![Chat with us](https://img.shields.io/badge/chat-IRC%20|%20Matrix%20|%20Web-blue.svg)](https://www.etebase.com/community-chat/)
# Installation
@@ -32,9 +34,9 @@ pip install -r requirements.txt
# Configuration
-If you are familiar with Django you can just edit the [settings file](etesync_server/settings.py)
+If you are familiar with Django you can just edit the [settings file](etebase_server/settings.py)
according to the [Django deployment checklist](https://docs.djangoproject.com/en/dev/howto/deployment/checklist/).
-If you are not, we also provide a simple [configuration file](https://github.com/etesync/server/blob/etebase/etebase-server.ini.example) for easy deployment which you can use.
+If you are not, we also provide a simple [configuration file](etebase-server.ini.example) for easy deployment which you can use.
To use the easy configuration file rename it to `etebase-server.ini` and place it either at the root of this repository or in `/etc/etebase-server`.
There is also a [wikipage](https://github.com/etesync/server/wiki/Basic-Setup-Etebase-(EteSync-v2)) detailing this basic setup.
@@ -84,10 +86,9 @@ Create yourself an admin user:
```
At this stage you need to create accounts to be used with the EteSync apps. To do that, please go to:
-`www.your-etesync-install.com/admin` and create a new user to be used with the service. Set a random
-password for the user such as `j3PmCRftyQMtM3eWvi8f`. No need to remember it, as it won't be used.
-Etebase uses a zero-knowledge proof for authentication, so the user will just create a password when
-creating the account from the apps.
+`www.your-etesync-install.com/admin` and create a new user to be used with the service. No need to set
+a password, as Etebase uses a zero-knowledge proof for authentication, so the user will just create
+a password when creating the account from the apps.
After this user has been created, you can use any of the EteSync apps to signup (or login) with the same username and
email in order to set up the account. The password used at that point will be used to setup the account.
diff --git a/django_etebase/app_settings.py b/django_etebase/app_settings.py
index 3c580b2..7c93f5f 100644
--- a/django_etebase/app_settings.py
+++ b/django_etebase/app_settings.py
@@ -21,18 +21,19 @@ class AppSettings:
def import_from_str(self, name):
from importlib import import_module
- path, prop = name.rsplit('.', 1)
+ path, prop = name.rsplit(".", 1)
mod = import_module(path)
return getattr(mod, prop)
def _setting(self, name, dflt):
from django.conf import settings
+
return getattr(settings, self.prefix + name, dflt)
@cached_property
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 = []
for perm in perms:
ret.append(self.import_from_str(perm))
@@ -40,8 +41,13 @@ class AppSettings:
@cached_property
def API_AUTHENTICATORS(self): # pylint: disable=invalid-name
- perms = self._setting("API_AUTHENTICATORS", ('rest_framework.authentication.TokenAuthentication',
- 'rest_framework.authentication.SessionAuthentication'))
+ perms = self._setting(
+ "API_AUTHENTICATORS",
+ (
+ "rest_framework.authentication.TokenAuthentication",
+ "rest_framework.authentication.SessionAuthentication",
+ ),
+ )
ret = []
for perm in perms:
ret.append(self.import_from_str(perm))
@@ -80,4 +86,4 @@ class AppSettings:
return self._setting("CHALLENGE_VALID_SECONDS", 60)
-app_settings = AppSettings('ETEBASE_')
+app_settings = AppSettings("ETEBASE_")
diff --git a/django_etebase/apps.py b/django_etebase/apps.py
index 286a708..84e4b6e 100644
--- a/django_etebase/apps.py
+++ b/django_etebase/apps.py
@@ -2,4 +2,4 @@ from django.apps import AppConfig
class DjangoEtebaseConfig(AppConfig):
- name = 'django_etebase'
+ name = "django_etebase"
diff --git a/django_etebase/drf_msgpack/apps.py b/django_etebase/drf_msgpack/apps.py
index 619e3e0..22ea2c1 100644
--- a/django_etebase/drf_msgpack/apps.py
+++ b/django_etebase/drf_msgpack/apps.py
@@ -2,4 +2,4 @@ from django.apps import AppConfig
class DrfMsgpackConfig(AppConfig):
- name = 'drf_msgpack'
+ name = "drf_msgpack"
diff --git a/django_etebase/drf_msgpack/parsers.py b/django_etebase/drf_msgpack/parsers.py
index 44cd33b..0504a76 100644
--- a/django_etebase/drf_msgpack/parsers.py
+++ b/django_etebase/drf_msgpack/parsers.py
@@ -5,10 +5,10 @@ from rest_framework.exceptions import ParseError
class MessagePackParser(BaseParser):
- media_type = 'application/msgpack'
+ media_type = "application/msgpack"
def parse(self, stream, media_type=None, parser_context=None):
try:
return msgpack.unpackb(stream.read(), raw=False)
except Exception as exc:
- raise ParseError('MessagePack parse error - %s' % str(exc))
+ raise ParseError("MessagePack parse error - %s" % str(exc))
diff --git a/django_etebase/drf_msgpack/renderers.py b/django_etebase/drf_msgpack/renderers.py
index 9445231..35a4afa 100644
--- a/django_etebase/drf_msgpack/renderers.py
+++ b/django_etebase/drf_msgpack/renderers.py
@@ -4,12 +4,12 @@ from rest_framework.renderers import BaseRenderer
class MessagePackRenderer(BaseRenderer):
- media_type = 'application/msgpack'
- format = 'msgpack'
- render_style = 'binary'
+ media_type = "application/msgpack"
+ format = "msgpack"
+ render_style = "binary"
charset = None
def render(self, data, media_type=None, renderer_context=None):
if data is None:
- return b''
+ return b""
return msgpack.packb(data, use_bin_type=True)
diff --git a/django_etebase/exceptions.py b/django_etebase/exceptions.py
index d05c4e5..f3aa08a 100644
--- a/django_etebase/exceptions.py
+++ b/django_etebase/exceptions.py
@@ -3,8 +3,7 @@ from rest_framework import serializers, status
class EtebaseValidationError(serializers.ValidationError):
def __init__(self, code, detail, status_code=status.HTTP_400_BAD_REQUEST):
- super().__init__({
- 'code': code,
- 'detail': detail,
- })
+ super().__init__(
+ {"code": code, "detail": detail,}
+ )
self.status_code = status_code
diff --git a/django_etebase/migrations/0001_initial.py b/django_etebase/migrations/0001_initial.py
index 69a9a91..86f0fa6 100644
--- a/django_etebase/migrations/0001_initial.py
+++ b/django_etebase/migrations/0001_initial.py
@@ -17,75 +17,159 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
- name='Collection',
+ name="Collection",
fields=[
- ('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()),
- ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ("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()),
+ ("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
- options={
- 'unique_together': {('uid', 'owner')},
- },
+ options={"unique_together": {("uid", "owner")},},
),
migrations.CreateModel(
- name='CollectionItem',
+ name="CollectionItem",
fields=[
- ('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()),
- ('encryptionKey', models.BinaryField(editable=True, null=True)),
- ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='django_etebase.Collection')),
+ ("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()),
+ ("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')},
- },
+ options={"unique_together": {("uid", "collection")},},
),
migrations.CreateModel(
- name='CollectionItemChunk',
+ name="CollectionItemChunk",
fields=[
- ('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)),
- ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='django_etebase.CollectionItem')),
+ ("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),
+ ),
+ (
+ "item",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="chunks",
+ to="django_etebase.CollectionItem",
+ ),
+ ),
],
),
migrations.CreateModel(
- name='CollectionItemRevision',
+ name="CollectionItemRevision",
fields=[
- ('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)),
- ('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')),
+ ("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)),
+ ("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')},
- },
+ options={"unique_together": {("item", "current")},},
),
migrations.CreateModel(
- name='RevisionChunkRelation',
+ name="RevisionChunkRelation",
fields=[
- ('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')),
+ ("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",
+ ),
+ ),
],
- options={
- 'ordering': ('id',),
- },
+ options={"ordering": ("id",),},
),
migrations.CreateModel(
- name='CollectionMember',
+ name="CollectionMember",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('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')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+ ("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",
+ ),
+ ),
+ ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
- options={
- 'unique_together': {('user', 'collection')},
- },
+ options={"unique_together": {("user", "collection")},},
),
]
diff --git a/django_etebase/migrations/0002_userinfo.py b/django_etebase/migrations/0002_userinfo.py
index 6da0bb8..6ddd9a5 100644
--- a/django_etebase/migrations/0002_userinfo.py
+++ b/django_etebase/migrations/0002_userinfo.py
@@ -8,18 +8,26 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
- ('myauth', '0001_initial'),
- ('django_etebase', '0001_initial'),
+ ("myauth", "0001_initial"),
+ ("django_etebase", "0001_initial"),
]
operations = [
migrations.CreateModel(
- name='UserInfo',
+ name="UserInfo",
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)),
- ('pubkey', models.BinaryField(editable=True)),
- ('salt', models.BinaryField(editable=True)),
+ (
+ "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)),
+ ("pubkey", models.BinaryField(editable=True)),
+ ("salt", models.BinaryField(editable=True)),
],
),
]
diff --git a/django_etebase/migrations/0003_collectioninvitation.py b/django_etebase/migrations/0003_collectioninvitation.py
index 8fd2066..1b416ab 100644
--- a/django_etebase/migrations/0003_collectioninvitation.py
+++ b/django_etebase/migrations/0003_collectioninvitation.py
@@ -10,22 +10,50 @@ class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('django_etebase', '0002_userinfo'),
+ ("django_etebase", "0002_userinfo"),
]
operations = [
migrations.CreateModel(
- name='CollectionInvitation',
+ name="CollectionInvitation",
fields=[
- ('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()),
- ('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)),
+ ("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()),
+ (
+ "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')},
- },
+ options={"unique_together": {("user", "fromMember")},},
),
]
diff --git a/django_etebase/migrations/0004_collectioninvitation_version.py b/django_etebase/migrations/0004_collectioninvitation_version.py
index 4052116..29ae3f1 100644
--- a/django_etebase/migrations/0004_collectioninvitation_version.py
+++ b/django_etebase/migrations/0004_collectioninvitation_version.py
@@ -6,13 +6,11 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0003_collectioninvitation'),
+ ("django_etebase", "0003_collectioninvitation"),
]
operations = [
migrations.AddField(
- model_name='collectioninvitation',
- name='version',
- field=models.PositiveSmallIntegerField(default=1),
+ model_name="collectioninvitation", name="version", field=models.PositiveSmallIntegerField(default=1),
),
]
diff --git a/django_etebase/migrations/0005_auto_20200526_1021.py b/django_etebase/migrations/0005_auto_20200526_1021.py
index da0dc33..3775277 100644
--- a/django_etebase/migrations/0005_auto_20200526_1021.py
+++ b/django_etebase/migrations/0005_auto_20200526_1021.py
@@ -6,13 +6,9 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0004_collectioninvitation_version'),
+ ("django_etebase", "0004_collectioninvitation_version"),
]
operations = [
- migrations.RenameField(
- model_name='userinfo',
- old_name='pubkey',
- new_name='loginPubkey',
- ),
+ migrations.RenameField(model_name="userinfo", old_name="pubkey", new_name="loginPubkey",),
]
diff --git a/django_etebase/migrations/0006_auto_20200526_1040.py b/django_etebase/migrations/0006_auto_20200526_1040.py
index b86a996..07b01cd 100644
--- a/django_etebase/migrations/0006_auto_20200526_1040.py
+++ b/django_etebase/migrations/0006_auto_20200526_1040.py
@@ -6,20 +6,20 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0005_auto_20200526_1021'),
+ ("django_etebase", "0005_auto_20200526_1021"),
]
operations = [
migrations.AddField(
- model_name='userinfo',
- name='encryptedSeckey',
- field=models.BinaryField(default=b'', editable=True),
+ model_name="userinfo",
+ name="encryptedSeckey",
+ field=models.BinaryField(default=b"", editable=True),
preserve_default=False,
),
migrations.AddField(
- model_name='userinfo',
- name='pubkey',
- field=models.BinaryField(default=b'', editable=True),
+ model_name="userinfo",
+ name="pubkey",
+ field=models.BinaryField(default=b"", editable=True),
preserve_default=False,
),
]
diff --git a/django_etebase/migrations/0007_auto_20200526_1336.py b/django_etebase/migrations/0007_auto_20200526_1336.py
index 79978c7..01afe45 100644
--- a/django_etebase/migrations/0007_auto_20200526_1336.py
+++ b/django_etebase/migrations/0007_auto_20200526_1336.py
@@ -7,33 +7,67 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0006_auto_20200526_1040'),
+ ("django_etebase", "0006_auto_20200526_1040"),
]
operations = [
migrations.AlterField(
- model_name='collection',
- name='uid',
- field=models.CharField(db_index=True, max_length=43, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9]*$')]),
+ model_name="collection",
+ name="uid",
+ field=models.CharField(
+ db_index=True,
+ max_length=43,
+ validators=[django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9]*$")],
+ ),
),
migrations.AlterField(
- model_name='collectioninvitation',
- 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}$')]),
+ model_name="collectioninvitation",
+ 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}$"
+ )
+ ],
+ ),
),
migrations.AlterField(
- model_name='collectionitem',
- 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]*$')]),
+ model_name="collectionitem",
+ 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]*$")],
+ ),
),
migrations.AlterField(
- model_name='collectionitemchunk',
- 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}$')]),
+ model_name="collectionitemchunk",
+ 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}$"
+ )
+ ],
+ ),
),
migrations.AlterField(
- model_name='collectionitemrevision',
- 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}$')]),
+ model_name="collectionitemrevision",
+ 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}$"
+ )
+ ],
+ ),
),
]
diff --git a/django_etebase/migrations/0008_auto_20200526_1535.py b/django_etebase/migrations/0008_auto_20200526_1535.py
index 12656c0..7bb83d5 100644
--- a/django_etebase/migrations/0008_auto_20200526_1535.py
+++ b/django_etebase/migrations/0008_auto_20200526_1535.py
@@ -9,20 +9,35 @@ import django_etebase.models
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0007_auto_20200526_1336'),
+ ("django_etebase", "0007_auto_20200526_1336"),
]
operations = [
migrations.CreateModel(
- name='Stoken',
+ name="Stoken",
fields=[
- ('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}$')])),
+ ("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}$"
+ )
+ ],
+ ),
+ ),
],
),
migrations.AddField(
- model_name='collectionitemrevision',
- name='stoken',
- field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etebase.Stoken'),
+ model_name="collectionitemrevision",
+ name="stoken",
+ field=models.OneToOneField(
+ null=True, on_delete=django.db.models.deletion.PROTECT, to="django_etebase.Stoken"
+ ),
),
]
diff --git a/django_etebase/migrations/0009_auto_20200526_1535.py b/django_etebase/migrations/0009_auto_20200526_1535.py
index a6ff498..0ab2f8c 100644
--- a/django_etebase/migrations/0009_auto_20200526_1535.py
+++ b/django_etebase/migrations/0009_auto_20200526_1535.py
@@ -4,8 +4,8 @@ from django.db import migrations
def create_stokens(apps, schema_editor):
- Stoken = apps.get_model('django_etebase', 'Stoken')
- CollectionItemRevision = apps.get_model('django_etebase', 'CollectionItemRevision')
+ Stoken = apps.get_model("django_etebase", "Stoken")
+ CollectionItemRevision = apps.get_model("django_etebase", "CollectionItemRevision")
for rev in CollectionItemRevision.objects.all():
rev.stoken = Stoken.objects.create()
@@ -15,7 +15,7 @@ def create_stokens(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0008_auto_20200526_1535'),
+ ("django_etebase", "0008_auto_20200526_1535"),
]
operations = [
diff --git a/django_etebase/migrations/0010_auto_20200526_1539.py b/django_etebase/migrations/0010_auto_20200526_1539.py
index 7ef0eca..204b97d 100644
--- a/django_etebase/migrations/0010_auto_20200526_1539.py
+++ b/django_etebase/migrations/0010_auto_20200526_1539.py
@@ -7,13 +7,13 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0009_auto_20200526_1535'),
+ ("django_etebase", "0009_auto_20200526_1535"),
]
operations = [
migrations.AlterField(
- model_name='collectionitemrevision',
- name='stoken',
- field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='django_etebase.Stoken'),
+ model_name="collectionitemrevision",
+ name="stoken",
+ field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to="django_etebase.Stoken"),
),
]
diff --git a/django_etebase/migrations/0011_collectionmember_stoken.py b/django_etebase/migrations/0011_collectionmember_stoken.py
index bafaea7..cbe8d06 100644
--- a/django_etebase/migrations/0011_collectionmember_stoken.py
+++ b/django_etebase/migrations/0011_collectionmember_stoken.py
@@ -7,13 +7,15 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0010_auto_20200526_1539'),
+ ("django_etebase", "0010_auto_20200526_1539"),
]
operations = [
migrations.AddField(
- model_name='collectionmember',
- name='stoken',
- field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etebase.Stoken'),
+ model_name="collectionmember",
+ name="stoken",
+ field=models.OneToOneField(
+ null=True, on_delete=django.db.models.deletion.PROTECT, to="django_etebase.Stoken"
+ ),
),
]
diff --git a/django_etebase/migrations/0012_auto_20200527_0743.py b/django_etebase/migrations/0012_auto_20200527_0743.py
index ab6adbc..1f58f82 100644
--- a/django_etebase/migrations/0012_auto_20200527_0743.py
+++ b/django_etebase/migrations/0012_auto_20200527_0743.py
@@ -4,8 +4,8 @@ from django.db import migrations
def create_stokens(apps, schema_editor):
- Stoken = apps.get_model('django_etebase', 'Stoken')
- CollectionMember = apps.get_model('django_etebase', 'CollectionMember')
+ Stoken = apps.get_model("django_etebase", "Stoken")
+ CollectionMember = apps.get_model("django_etebase", "CollectionMember")
for member in CollectionMember.objects.all():
member.stoken = Stoken.objects.create()
@@ -15,7 +15,7 @@ def create_stokens(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0011_collectionmember_stoken'),
+ ("django_etebase", "0011_collectionmember_stoken"),
]
operations = [
diff --git a/django_etebase/migrations/0013_collectionmemberremoved.py b/django_etebase/migrations/0013_collectionmemberremoved.py
index 2641c03..4481e80 100644
--- a/django_etebase/migrations/0013_collectionmemberremoved.py
+++ b/django_etebase/migrations/0013_collectionmemberremoved.py
@@ -9,20 +9,30 @@ class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('django_etebase', '0012_auto_20200527_0743'),
+ ("django_etebase", "0012_auto_20200527_0743"),
]
operations = [
migrations.CreateModel(
- name='CollectionMemberRemoved',
+ name="CollectionMemberRemoved",
fields=[
- ('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')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ("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"
+ ),
+ ),
+ ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
- options={
- 'unique_together': {('user', 'collection')},
- },
+ options={"unique_together": {("user", "collection")},},
),
]
diff --git a/django_etebase/migrations/0014_auto_20200602_1558.py b/django_etebase/migrations/0014_auto_20200602_1558.py
index d1a555d..42bed52 100644
--- a/django_etebase/migrations/0014_auto_20200602_1558.py
+++ b/django_etebase/migrations/0014_auto_20200602_1558.py
@@ -6,13 +6,9 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0013_collectionmemberremoved'),
+ ("django_etebase", "0013_collectionmemberremoved"),
]
operations = [
- migrations.RenameField(
- model_name='userinfo',
- old_name='encryptedSeckey',
- new_name='encryptedContent',
- ),
+ migrations.RenameField(model_name="userinfo", old_name="encryptedSeckey", new_name="encryptedContent",),
]
diff --git a/django_etebase/migrations/0015_collectionitemrevision_salt.py b/django_etebase/migrations/0015_collectionitemrevision_salt.py
index 7f3dd71..c4dc3e9 100644
--- a/django_etebase/migrations/0015_collectionitemrevision_salt.py
+++ b/django_etebase/migrations/0015_collectionitemrevision_salt.py
@@ -6,13 +6,11 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0014_auto_20200602_1558'),
+ ("django_etebase", "0014_auto_20200602_1558"),
]
operations = [
migrations.AddField(
- model_name='collectionitemrevision',
- name='salt',
- field=models.BinaryField(default=b'', editable=True),
+ model_name="collectionitemrevision", name="salt", field=models.BinaryField(default=b"", editable=True),
),
]
diff --git a/django_etebase/migrations/0016_auto_20200623_0820.py b/django_etebase/migrations/0016_auto_20200623_0820.py
index 2c11157..a273b0d 100644
--- a/django_etebase/migrations/0016_auto_20200623_0820.py
+++ b/django_etebase/migrations/0016_auto_20200623_0820.py
@@ -7,25 +7,21 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0015_collectionitemrevision_salt'),
+ ("django_etebase", "0015_collectionitemrevision_salt"),
]
operations = [
migrations.AddField(
- model_name='collection',
- name='main_item',
- field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parent', to='django_etebase.CollectionItem'),
- ),
- migrations.AlterUniqueTogether(
- name='collection',
- unique_together=set(),
- ),
- migrations.RemoveField(
- model_name='collection',
- name='uid',
- ),
- migrations.RemoveField(
- model_name='collection',
- name='version',
+ model_name="collection",
+ name="main_item",
+ field=models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="parent",
+ to="django_etebase.CollectionItem",
+ ),
),
+ migrations.AlterUniqueTogether(name="collection", unique_together=set(),),
+ migrations.RemoveField(model_name="collection", name="uid",),
+ migrations.RemoveField(model_name="collection", name="version",),
]
diff --git a/django_etebase/migrations/0017_auto_20200623_0958.py b/django_etebase/migrations/0017_auto_20200623_0958.py
index e244b13..dc599aa 100644
--- a/django_etebase/migrations/0017_auto_20200623_0958.py
+++ b/django_etebase/migrations/0017_auto_20200623_0958.py
@@ -8,18 +8,27 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0016_auto_20200623_0820'),
+ ("django_etebase", "0016_auto_20200623_0820"),
]
operations = [
migrations.AlterField(
- model_name='collection',
- name='main_item',
- field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent', to='django_etebase.CollectionItem'),
+ model_name="collection",
+ name="main_item",
+ field=models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="parent",
+ to="django_etebase.CollectionItem",
+ ),
),
migrations.AlterField(
- model_name='collectionitem',
- 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]*$')]),
+ model_name="collectionitem",
+ 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]*$")],
+ ),
),
]
diff --git a/django_etebase/migrations/0018_auto_20200624_0748.py b/django_etebase/migrations/0018_auto_20200624_0748.py
index ec59e0c..d2cdf5a 100644
--- a/django_etebase/migrations/0018_auto_20200624_0748.py
+++ b/django_etebase/migrations/0018_auto_20200624_0748.py
@@ -7,13 +7,19 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0017_auto_20200623_0958'),
+ ("django_etebase", "0017_auto_20200623_0958"),
]
operations = [
migrations.AlterField(
- model_name='collectionitem',
- 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\\-_]*$')]),
+ model_name="collectionitem",
+ 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\\-_]*$")
+ ],
+ ),
),
]
diff --git a/django_etebase/migrations/0019_auto_20200626_0748.py b/django_etebase/migrations/0019_auto_20200626_0748.py
index 991ca50..175e4d0 100644
--- a/django_etebase/migrations/0019_auto_20200626_0748.py
+++ b/django_etebase/migrations/0019_auto_20200626_0748.py
@@ -7,13 +7,19 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0018_auto_20200624_0748'),
+ ("django_etebase", "0018_auto_20200624_0748"),
]
operations = [
migrations.AlterField(
- model_name='collectionitemchunk',
- 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\\-_]*$')]),
+ model_name="collectionitemchunk",
+ 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\\-_]*$")
+ ],
+ ),
),
]
diff --git a/django_etebase/migrations/0020_remove_collectionitemrevision_salt.py b/django_etebase/migrations/0020_remove_collectionitemrevision_salt.py
index 2df32bf..21d0337 100644
--- a/django_etebase/migrations/0020_remove_collectionitemrevision_salt.py
+++ b/django_etebase/migrations/0020_remove_collectionitemrevision_salt.py
@@ -6,12 +6,9 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0019_auto_20200626_0748'),
+ ("django_etebase", "0019_auto_20200626_0748"),
]
operations = [
- migrations.RemoveField(
- model_name='collectionitemrevision',
- name='salt',
- ),
+ migrations.RemoveField(model_name="collectionitemrevision", name="salt",),
]
diff --git a/django_etebase/migrations/0021_auto_20200626_0913.py b/django_etebase/migrations/0021_auto_20200626_0913.py
index b890384..3bb6e21 100644
--- a/django_etebase/migrations/0021_auto_20200626_0913.py
+++ b/django_etebase/migrations/0021_auto_20200626_0913.py
@@ -8,33 +8,66 @@ import django_etebase.models
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0020_remove_collectionitemrevision_salt'),
+ ("django_etebase", "0020_remove_collectionitemrevision_salt"),
]
operations = [
migrations.AlterField(
- model_name='collectioninvitation',
- 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,}$')]),
+ model_name="collectioninvitation",
+ 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,}$")
+ ],
+ ),
),
migrations.AlterField(
- model_name='collectionitem',
- 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,}$')]),
+ model_name="collectionitem",
+ 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,}$")
+ ],
+ ),
),
migrations.AlterField(
- model_name='collectionitemchunk',
- 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,}$')]),
+ model_name="collectionitemchunk",
+ 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,}$")
+ ],
+ ),
),
migrations.AlterField(
- model_name='collectionitemrevision',
- 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,}$')]),
+ model_name="collectionitemrevision",
+ 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,}$")
+ ],
+ ),
),
migrations.AlterField(
- model_name='stoken',
- 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,}$')]),
+ model_name="stoken",
+ 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,}$")
+ ],
+ ),
),
]
diff --git a/django_etebase/migrations/0022_auto_20200804_1059.py b/django_etebase/migrations/0022_auto_20200804_1059.py
index c47e562..60af33f 100644
--- a/django_etebase/migrations/0022_auto_20200804_1059.py
+++ b/django_etebase/migrations/0022_auto_20200804_1059.py
@@ -6,12 +6,9 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0021_auto_20200626_0913'),
+ ("django_etebase", "0021_auto_20200626_0913"),
]
operations = [
- migrations.AlterUniqueTogether(
- name='collectionitemchunk',
- unique_together={('item', 'uid')},
- ),
+ migrations.AlterUniqueTogether(name="collectionitemchunk", unique_together={("item", "uid")},),
]
diff --git a/django_etebase/migrations/0023_collectionitemchunk_collection.py b/django_etebase/migrations/0023_collectionitemchunk_collection.py
index b5d6841..314302f 100644
--- a/django_etebase/migrations/0023_collectionitemchunk_collection.py
+++ b/django_etebase/migrations/0023_collectionitemchunk_collection.py
@@ -7,13 +7,18 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0022_auto_20200804_1059'),
+ ("django_etebase", "0022_auto_20200804_1059"),
]
operations = [
migrations.AddField(
- model_name='collectionitemchunk',
- name='collection',
- field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='django_etebase.Collection'),
+ model_name="collectionitemchunk",
+ name="collection",
+ field=models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="chunks",
+ to="django_etebase.Collection",
+ ),
),
]
diff --git a/django_etebase/migrations/0024_auto_20200804_1209.py b/django_etebase/migrations/0024_auto_20200804_1209.py
index 54c80a3..955a4f9 100644
--- a/django_etebase/migrations/0024_auto_20200804_1209.py
+++ b/django_etebase/migrations/0024_auto_20200804_1209.py
@@ -4,7 +4,7 @@ from django.db import migrations
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():
chunk.collection = chunk.item.collection
@@ -14,7 +14,7 @@ def change_chunk_to_collections(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0023_collectionitemchunk_collection'),
+ ("django_etebase", "0023_collectionitemchunk_collection"),
]
operations = [
diff --git a/django_etebase/migrations/0025_auto_20200804_1216.py b/django_etebase/migrations/0025_auto_20200804_1216.py
index 8849f53..91bf4c8 100644
--- a/django_etebase/migrations/0025_auto_20200804_1216.py
+++ b/django_etebase/migrations/0025_auto_20200804_1216.py
@@ -7,21 +7,17 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0024_auto_20200804_1209'),
+ ("django_etebase", "0024_auto_20200804_1209"),
]
operations = [
migrations.AlterField(
- model_name='collectionitemchunk',
- name='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',
+ model_name="collectionitemchunk",
+ name="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",),
]
diff --git a/django_etebase/migrations/0026_auto_20200907_0752.py b/django_etebase/migrations/0026_auto_20200907_0752.py
index 38c0b92..3283654 100644
--- a/django_etebase/migrations/0026_auto_20200907_0752.py
+++ b/django_etebase/migrations/0026_auto_20200907_0752.py
@@ -6,18 +6,10 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0025_auto_20200804_1216'),
+ ("django_etebase", "0025_auto_20200804_1216"),
]
operations = [
- migrations.RenameField(
- model_name='collectioninvitation',
- old_name='accessLevel',
- new_name='accessLevelOld',
- ),
- migrations.RenameField(
- model_name='collectionmember',
- old_name='accessLevel',
- new_name='accessLevelOld',
- ),
+ migrations.RenameField(model_name="collectioninvitation", old_name="accessLevel", new_name="accessLevelOld",),
+ migrations.RenameField(model_name="collectionmember", old_name="accessLevel", new_name="accessLevelOld",),
]
diff --git a/django_etebase/migrations/0027_auto_20200907_0752.py b/django_etebase/migrations/0027_auto_20200907_0752.py
index d822d3d..21f607f 100644
--- a/django_etebase/migrations/0027_auto_20200907_0752.py
+++ b/django_etebase/migrations/0027_auto_20200907_0752.py
@@ -6,18 +6,18 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0026_auto_20200907_0752'),
+ ("django_etebase", "0026_auto_20200907_0752"),
]
operations = [
migrations.AddField(
- model_name='collectioninvitation',
- name='accessLevel',
- field=models.IntegerField(choices=[(0, 'Read Only'), (1, 'Admin'), (2, 'Read Write')], default=0),
+ model_name="collectioninvitation",
+ name="accessLevel",
+ field=models.IntegerField(choices=[(0, "Read Only"), (1, "Admin"), (2, "Read Write")], default=0),
),
migrations.AddField(
- model_name='collectionmember',
- name='accessLevel',
- field=models.IntegerField(choices=[(0, 'Read Only'), (1, 'Admin'), (2, 'Read Write')], default=0),
+ model_name="collectionmember",
+ name="accessLevel",
+ field=models.IntegerField(choices=[(0, "Read Only"), (1, "Admin"), (2, "Read Write")], default=0),
),
]
diff --git a/django_etebase/migrations/0028_auto_20200907_0754.py b/django_etebase/migrations/0028_auto_20200907_0754.py
index cb62e63..24c6246 100644
--- a/django_etebase/migrations/0028_auto_20200907_0754.py
+++ b/django_etebase/migrations/0028_auto_20200907_0754.py
@@ -6,24 +6,24 @@ from django_etebase.models import AccessLevels
def change_access_level_to_int(apps, schema_editor):
- CollectionMember = apps.get_model('django_etebase', 'CollectionMember')
- CollectionInvitation = apps.get_model('django_etebase', 'CollectionInvitation')
+ CollectionMember = apps.get_model("django_etebase", "CollectionMember")
+ CollectionInvitation = apps.get_model("django_etebase", "CollectionInvitation")
for member in CollectionMember.objects.all():
- if member.accessLevelOld == 'adm':
+ if member.accessLevelOld == "adm":
member.accessLevel = AccessLevels.ADMIN
- elif member.accessLevelOld == 'rw':
+ elif member.accessLevelOld == "rw":
member.accessLevel = AccessLevels.READ_WRITE
- elif member.accessLevelOld == 'ro':
+ elif member.accessLevelOld == "ro":
member.accessLevel = AccessLevels.READ_ONLY
member.save()
for invitation in CollectionInvitation.objects.all():
- if invitation.accessLevelOld == 'adm':
+ if invitation.accessLevelOld == "adm":
invitation.accessLevel = AccessLevels.ADMIN
- elif invitation.accessLevelOld == 'rw':
+ elif invitation.accessLevelOld == "rw":
invitation.accessLevel = AccessLevels.READ_WRITE
- elif invitation.accessLevelOld == 'ro':
+ elif invitation.accessLevelOld == "ro":
invitation.accessLevel = AccessLevels.READ_ONLY
invitation.save()
@@ -31,7 +31,7 @@ def change_access_level_to_int(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0027_auto_20200907_0752'),
+ ("django_etebase", "0027_auto_20200907_0752"),
]
operations = [
diff --git a/django_etebase/migrations/0029_auto_20200907_0801.py b/django_etebase/migrations/0029_auto_20200907_0801.py
index 7cd54d4..f3bfe61 100644
--- a/django_etebase/migrations/0029_auto_20200907_0801.py
+++ b/django_etebase/migrations/0029_auto_20200907_0801.py
@@ -6,16 +6,10 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0028_auto_20200907_0754'),
+ ("django_etebase", "0028_auto_20200907_0754"),
]
operations = [
- migrations.RemoveField(
- model_name='collectioninvitation',
- name='accessLevelOld',
- ),
- migrations.RemoveField(
- model_name='collectionmember',
- name='accessLevelOld',
- ),
+ migrations.RemoveField(model_name="collectioninvitation", name="accessLevelOld",),
+ migrations.RemoveField(model_name="collectionmember", name="accessLevelOld",),
]
diff --git a/django_etebase/migrations/0030_auto_20200922_0832.py b/django_etebase/migrations/0030_auto_20200922_0832.py
index d5fa95d..a689251 100644
--- a/django_etebase/migrations/0030_auto_20200922_0832.py
+++ b/django_etebase/migrations/0030_auto_20200922_0832.py
@@ -7,13 +7,18 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0029_auto_20200907_0801'),
+ ("django_etebase", "0029_auto_20200907_0801"),
]
operations = [
migrations.AlterField(
- model_name='collection',
- name='main_item',
- field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent', to='django_etebase.collectionitem'),
+ model_name="collection",
+ name="main_item",
+ field=models.OneToOneField(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="parent",
+ to="django_etebase.collectionitem",
+ ),
),
]
diff --git a/django_etebase/migrations/0031_auto_20201013_1336.py b/django_etebase/migrations/0031_auto_20201013_1336.py
index ca45dd4..ae6e5e5 100644
--- a/django_etebase/migrations/0031_auto_20201013_1336.py
+++ b/django_etebase/migrations/0031_auto_20201013_1336.py
@@ -9,21 +9,23 @@ class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('django_etebase', '0030_auto_20200922_0832'),
+ ("django_etebase", "0030_auto_20200922_0832"),
]
operations = [
migrations.CreateModel(
- name='CollectionType',
+ name="CollectionType",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('uid', models.BinaryField(db_index=True, editable=True)),
- ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+ ("uid", models.BinaryField(db_index=True, editable=True)),
+ ("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
- model_name='collectionmember',
- name='collectionType',
- field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etebase.collectiontype'),
+ model_name="collectionmember",
+ name="collectionType",
+ field=models.ForeignKey(
+ null=True, on_delete=django.db.models.deletion.PROTECT, to="django_etebase.collectiontype"
+ ),
),
]
diff --git a/django_etebase/migrations/0032_auto_20201013_1409.py b/django_etebase/migrations/0032_auto_20201013_1409.py
index 5594006..2bb3cb0 100644
--- a/django_etebase/migrations/0032_auto_20201013_1409.py
+++ b/django_etebase/migrations/0032_auto_20201013_1409.py
@@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('django_etebase', '0031_auto_20201013_1336'),
+ ("django_etebase", "0031_auto_20201013_1336"),
]
operations = [
migrations.AlterField(
- model_name='collectiontype',
- name='uid',
+ model_name="collectiontype",
+ name="uid",
field=models.BinaryField(db_index=True, editable=True, unique=True),
),
]
diff --git a/django_etebase/models.py b/django_etebase/models.py
index 0036884..691947d 100644
--- a/django_etebase/models.py
+++ b/django_etebase/models.py
@@ -27,7 +27,7 @@ from . import app_settings
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):
@@ -36,7 +36,7 @@ class CollectionType(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)
def __str__(self):
@@ -56,38 +56,45 @@ class Collection(models.Model):
@cached_property
def stoken(self):
- stoken = Stoken.objects.filter(
- Q(collectionitemrevision__item__collection=self) | Q(collectionmember__collection=self)
- ).order_by('id').last()
+ stoken = (
+ Stoken.objects.filter(
+ Q(collectionitemrevision__item__collection=self) | Q(collectionmember__collection=self)
+ )
+ .order_by("id")
+ .last()
+ )
if stoken is None:
- raise Exception('stoken is None. Should never happen')
+ raise Exception("stoken is None. Should never happen")
return stoken.uid
def validate_unique(self, exclude=None):
super().validate_unique(exclude=exclude)
- if exclude is None or 'main_item' in exclude:
+ if exclude is None or "main_item" in exclude:
return
- if self.__class__.objects.filter(owner=self.owner, main_item__uid=self.main_item.uid) \
- .exclude(id=self.id).exists():
- raise EtebaseValidationError('unique_uid', 'Collection with this uid already exists',
- status_code=status.HTTP_409_CONFLICT)
+ if (
+ self.__class__.objects.filter(owner=self.owner, main_item__uid=self.main_item.uid)
+ .exclude(id=self.id)
+ .exists()
+ ):
+ raise EtebaseValidationError(
+ "unique_uid", "Collection with this uid already exists", status_code=status.HTTP_409_CONFLICT
+ )
class CollectionItem(models.Model):
- uid = models.CharField(db_index=True, blank=False,
- max_length=43, validators=[UidValidator])
- collection = models.ForeignKey(Collection, related_name='items', on_delete=models.CASCADE)
+ uid = models.CharField(db_index=True, blank=False, max_length=43, validators=[UidValidator])
+ collection = models.ForeignKey(Collection, related_name="items", on_delete=models.CASCADE)
version = models.PositiveSmallIntegerField()
encryptionKey = models.BinaryField(editable=True, blank=False, null=True)
class Meta:
- unique_together = ('uid', 'collection')
+ unique_together = ("uid", "collection")
def __str__(self):
- return '{} {}'.format(self.uid, self.collection.uid)
+ return "{} {}".format(self.uid, self.collection.uid)
@cached_property
def content(self):
@@ -107,53 +114,60 @@ def chunk_directory_path(instance, filename):
user_id = col.owner.id
uid_prefix = 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):
- uid = models.CharField(db_index=True, blank=False, null=False,
- max_length=60, validators=[UidValidator])
- collection = models.ForeignKey(Collection, related_name='chunks', on_delete=models.CASCADE)
+ uid = models.CharField(db_index=True, blank=False, null=False, max_length=60, validators=[UidValidator])
+ collection = models.ForeignKey(Collection, related_name="chunks", on_delete=models.CASCADE)
chunkFile = models.FileField(upload_to=chunk_directory_path, max_length=150, unique=True)
def __str__(self):
return self.uid
class Meta:
- unique_together = ('collection', 'uid')
+ unique_together = ("collection", "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):
- uid = models.CharField(db_index=True, unique=True, blank=False, null=False, default=generate_stoken_uid,
- max_length=43, validators=[UidValidator])
+ uid = models.CharField(
+ db_index=True,
+ unique=True,
+ blank=False,
+ null=False,
+ default=generate_stoken_uid,
+ max_length=43,
+ validators=[UidValidator],
+ )
class CollectionItemRevision(models.Model):
stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT)
- uid = models.CharField(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)
+ uid = models.CharField(
+ 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)
meta = models.BinaryField(editable=True, blank=False, null=False)
current = models.BooleanField(db_index=True, default=True, null=True)
deleted = models.BooleanField(default=False)
class Meta:
- unique_together = ('item', 'current')
+ unique_together = ("item", "current")
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):
- chunk = models.ForeignKey(CollectionItemChunk, related_name='revisions_relation', on_delete=models.CASCADE)
- revision = models.ForeignKey(CollectionItemRevision, related_name='chunks_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)
class Meta:
- ordering = ('id', )
+ ordering = ("id",)
class AccessLevels(models.IntegerChoices):
@@ -164,28 +178,22 @@ class AccessLevels(models.IntegerChoices):
class CollectionMember(models.Model):
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)
encryptionKey = models.BinaryField(editable=True, blank=False, null=False)
collectionType = models.ForeignKey(CollectionType, on_delete=models.PROTECT, null=True)
- accessLevel = models.IntegerField(
- choices=AccessLevels.choices,
- default=AccessLevels.READ_ONLY,
- )
+ accessLevel = models.IntegerField(choices=AccessLevels.choices, default=AccessLevels.READ_ONLY,)
class Meta:
- unique_together = ('user', 'collection')
+ unique_together = ("user", "collection")
def __str__(self):
- return '{} {}'.format(self.collection.uid, self.user)
+ return "{} {}".format(self.collection.uid, self.user)
def revoke(self):
with transaction.atomic():
CollectionMemberRemoved.objects.update_or_create(
- collection=self.collection, user=self.user,
- defaults={
- 'stoken': Stoken.objects.create(),
- },
+ collection=self.collection, user=self.user, defaults={"stoken": Stoken.objects.create(),},
)
self.delete()
@@ -193,36 +201,32 @@ class CollectionMember(models.Model):
class CollectionMemberRemoved(models.Model):
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)
class Meta:
- unique_together = ('user', 'collection')
+ unique_together = ("user", "collection")
def __str__(self):
- return '{} {}'.format(self.collection.uid, self.user)
+ return "{} {}".format(self.collection.uid, self.user)
class CollectionInvitation(models.Model):
- uid = models.CharField(db_index=True, blank=False, null=False,
- max_length=43, validators=[UidValidator])
+ uid = models.CharField(db_index=True, blank=False, null=False, max_length=43, validators=[UidValidator])
version = models.PositiveSmallIntegerField(default=1)
fromMember = models.ForeignKey(CollectionMember, on_delete=models.CASCADE)
# 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
- 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)
- accessLevel = models.IntegerField(
- choices=AccessLevels.choices,
- default=AccessLevels.READ_ONLY,
- )
+ accessLevel = models.IntegerField(choices=AccessLevels.choices, default=AccessLevels.READ_ONLY,)
class Meta:
- unique_together = ('user', 'fromMember')
+ unique_together = ("user", "fromMember")
def __str__(self):
- return '{} {}'.format(self.fromMember.collection.uid, self.user)
+ return "{} {}".format(self.fromMember.collection.uid, self.user)
@cached_property
def collection(self):
diff --git a/django_etebase/parsers.py b/django_etebase/parsers.py
index 1ca1a70..c7fe58c 100644
--- a/django_etebase/parsers.py
+++ b/django_etebase/parsers.py
@@ -5,11 +5,12 @@ class ChunkUploadParser(FileUploadParser):
"""
Parser for chunk upload data.
"""
- media_type = 'application/octet-stream'
+
+ media_type = "application/octet-stream"
def get_filename(self, stream, media_type, parser_context):
"""
Detects the uploaded file name.
"""
- view = parser_context['view']
- return parser_context['kwargs'][view.lookup_field]
+ view = parser_context["view"]
+ return parser_context["kwargs"][view.lookup_field]
diff --git a/django_etebase/permissions.py b/django_etebase/permissions.py
index c624404..3c77d06 100644
--- a/django_etebase/permissions.py
+++ b/django_etebase/permissions.py
@@ -25,13 +25,14 @@ class IsCollectionAdmin(permissions.BasePermission):
"""
Custom permission to only allow owners of a collection to view it
"""
+
message = {
- 'detail': 'Only collection admins can perform this operation.',
- 'code': 'admin_access_required',
+ "detail": "Only collection admins can perform this operation.",
+ "code": "admin_access_required",
}
def has_permission(self, request, view):
- collection_uid = view.kwargs['collection_uid']
+ collection_uid = view.kwargs["collection_uid"]
try:
collection = view.get_collection_queryset().get(main_item__uid=collection_uid)
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
"""
+
message = {
- 'detail': 'Only collection admins can edit collections.',
- 'code': 'admin_access_required',
+ "detail": "Only collection admins can edit collections.",
+ "code": "admin_access_required",
}
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
if collection_uid is None:
@@ -71,13 +73,14 @@ class HasWriteAccessOrReadOnly(permissions.BasePermission):
"""
Custom permission to restrict write
"""
+
message = {
- 'detail': 'You need write access to write to this collection',
- 'code': 'no_write_access',
+ "detail": "You need write access to write to this collection",
+ "code": "no_write_access",
}
def has_permission(self, request, view):
- collection_uid = view.kwargs['collection_uid']
+ collection_uid = view.kwargs["collection_uid"]
try:
collection = view.get_collection_queryset().get(main_item__uid=collection_uid)
if request.method in permissions.SAFE_METHODS:
diff --git a/django_etebase/renderers.py b/django_etebase/renderers.py
index 43c1a0d..0d359d3 100644
--- a/django_etebase/renderers.py
+++ b/django_etebase/renderers.py
@@ -15,4 +15,5 @@ class JSONRenderer(DRFJSONRenderer):
"""
Renderer which serializes to JSON with support for our base64
"""
+
encoder_class = JSONEncoder
diff --git a/django_etebase/serializers.py b/django_etebase/serializers.py
index 97dcd64..ef3b296 100644
--- a/django_etebase/serializers.py
+++ b/django_etebase/serializers.py
@@ -29,7 +29,11 @@ User = get_user_model()
def process_revisions_for_item(item, revision_data):
chunks_objs = []
- chunks = revision_data.pop('chunks_relation')
+ chunks = revision_data.pop("chunks_relation")
+
+ revision = models.CollectionItemRevision(**revision_data, item=item)
+ revision.validate_unique() # Verify there aren't any validation issues
+
for chunk in chunks:
uid = chunk[0]
chunk_obj = models.CollectionItemChunk.objects.filter(uid=uid).first()
@@ -38,24 +42,25 @@ def process_revisions_for_item(item, revision_data):
# If the chunk already exists we assume it's fine. Otherwise, we upload it.
if chunk_obj is None:
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()
else:
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)
stoken = models.Stoken.objects.create()
+ revision.stoken = stoken
+ revision.save()
- revision = models.CollectionItemRevision.objects.create(**revision_data, item=item, stoken=stoken)
for chunk in chunks_objs:
models.RevisionChunkRelation.objects.create(chunk=chunk, revision=revision)
return revision
def b64encode(value):
- return base64.urlsafe_b64encode(value).decode('ascii').strip('=')
+ return base64.urlsafe_b64encode(value).decode("ascii").strip("=")
def b64decode(data):
@@ -80,7 +85,7 @@ class BinaryBase64Field(serializers.Field):
class CollectionEncryptionKeyField(BinaryBase64Field):
def get_attribute(self, instance):
- request = self.context.get('request', None)
+ request = self.context.get("request", None)
if request is not None:
return instance.members.get(user=request.user).encryptionKey
return None
@@ -88,7 +93,7 @@ class CollectionEncryptionKeyField(BinaryBase64Field):
class CollectionTypeField(BinaryBase64Field):
def get_attribute(self, instance):
- request = self.context.get('request', None)
+ request = self.context.get("request", None)
if request is not None:
collection_type = instance.members.get(user=request.user).collectionType
return collection_type and collection_type.uid
@@ -97,7 +102,7 @@ class CollectionTypeField(BinaryBase64Field):
class UserSlugRelatedField(serializers.SlugRelatedField):
def get_queryset(self):
- view = self.context.get('view', None)
+ view = self.context.get("view", None)
return get_user_queryset(super().get_queryset(), view)
def __init__(self, **kwargs):
@@ -110,15 +115,15 @@ class UserSlugRelatedField(serializers.SlugRelatedField):
class ChunksField(serializers.RelatedField):
def to_representation(self, obj):
obj = obj.chunk
- if self.context.get('prefetch') == 'auto':
- with open(obj.chunkFile.path, 'rb') as f:
+ if self.context.get("prefetch") == "auto":
+ with open(obj.chunkFile.path, "rb") as f:
return (obj.uid, f.read())
else:
- return (obj.uid, )
+ return (obj.uid,)
def to_internal_value(self, data):
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]))
@@ -128,18 +133,12 @@ class BetterErrorsMixin:
nice = []
errors = super().errors
for error_type in errors:
- if error_type == 'non_field_errors':
- nice.extend(
- self.flatten_errors(None, errors[error_type])
- )
+ if error_type == "non_field_errors":
+ nice.extend(self.flatten_errors(None, errors[error_type]))
else:
- nice.extend(
- self.flatten_errors(error_type, errors[error_type])
- )
+ nice.extend(self.flatten_errors(error_type, errors[error_type]))
if nice:
- return {'code': 'field_errors',
- 'detail': 'Field validations failed.',
- 'errors': nice}
+ return {"code": "field_errors", "detail": "Field validations failed.", "errors": nice}
return {}
def flatten_errors(self, field_name, errors):
@@ -150,52 +149,51 @@ class BetterErrorsMixin:
ret.extend(self.flatten_errors("{}.{}".format(field_name, error_key), error))
else:
for error in errors:
- if hasattr(error, 'detail'):
+ if hasattr(error, "detail"):
message = error.detail[0]
- elif hasattr(error, 'message'):
+ elif hasattr(error, "message"):
message = error.message
else:
message = str(error)
- ret.append({
- 'field': field_name,
- 'code': error.code,
- 'detail': message,
- })
+ ret.append(
+ {"field": field_name, "code": error.code, "detail": message,}
+ )
return ret
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)
- elif not hasattr(err, 'message'):
+ elif not hasattr(err, "message"):
errors = self.flatten_errors(prefix, err.error_list)
else:
raise EtebaseValidationError(err.code, err.message)
- raise serializers.ValidationError({
- 'code': 'field_errors',
- 'detail': 'Field validations failed.',
- 'errors': errors,
- })
+ raise serializers.ValidationError(
+ {"code": "field_errors", "detail": "Field validations failed.", "errors": errors,}
+ )
class CollectionItemChunkSerializer(BetterErrorsMixin, serializers.ModelSerializer):
class Meta:
model = models.CollectionItemChunk
- fields = ('uid', 'chunkFile')
+ fields = ("uid", "chunkFile")
class CollectionItemRevisionSerializer(BetterErrorsMixin, serializers.ModelSerializer):
chunks = ChunksField(
- source='chunks_relation',
+ source="chunks_relation",
queryset=models.RevisionChunkRelation.objects.all(),
- style={'base_template': 'input.html'},
- many=True
+ style={"base_template": "input.html"},
+ many=True,
)
meta = BinaryBase64Field()
class Meta:
model = models.CollectionItemRevision
- fields = ('chunks', 'meta', 'uid', 'deleted')
+ fields = ("chunks", "meta", "uid", "deleted")
+ extra_kwargs = {
+ "uid": {"validators": []}, # We deal with it in the serializers
+ }
class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer):
@@ -205,14 +203,14 @@ class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer):
class Meta:
model = models.CollectionItem
- fields = ('uid', 'version', 'encryptionKey', 'content', 'etag')
+ fields = ("uid", "version", "encryptionKey", "content", "etag")
def create(self, validated_data):
"""Function that's called when this serializer creates an item"""
- validate_etag = self.context.get('validate_etag', False)
- etag = validated_data.pop('etag')
- revision_data = validated_data.pop('content')
- uid = validated_data.pop('uid')
+ validate_etag = self.context.get("validate_etag", False)
+ etag = validated_data.pop("etag")
+ revision_data = validated_data.pop("content")
+ uid = validated_data.pop("uid")
Model = self.__class__.Meta.model
@@ -220,9 +218,16 @@ class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer):
instance, created = Model.objects.get_or_create(uid=uid, defaults=validated_data)
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 cur_etag == revision_data.get("uid"):
+ return instance
+
if validate_etag and cur_etag != etag:
- raise EtebaseValidationError('wrong_etag', 'Wrong etag. Expected {} got {}'.format(cur_etag, etag),
- status_code=status.HTTP_409_CONFLICT)
+ raise EtebaseValidationError(
+ "wrong_etag",
+ "Wrong etag. Expected {} got {}".format(cur_etag, etag),
+ status_code=status.HTTP_409_CONFLICT,
+ )
if not created:
# We don't have to use select_for_update here because the unique constraint on current guards against
@@ -231,7 +236,10 @@ class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer):
current_revision.current = None
current_revision.save()
- process_revisions_for_item(instance, revision_data)
+ try:
+ process_revisions_for_item(instance, revision_data)
+ except django_exceptions.ValidationError as e:
+ self.transform_validation_error("content", e)
return instance
@@ -245,14 +253,17 @@ class CollectionItemDepSerializer(BetterErrorsMixin, serializers.ModelSerializer
class Meta:
model = models.CollectionItem
- fields = ('uid', 'etag')
+ fields = ("uid", "etag")
def validate(self, data):
- item = self.__class__.Meta.model.objects.get(uid=data['uid'])
- etag = data['etag']
+ item = self.__class__.Meta.model.objects.get(uid=data["uid"])
+ etag = data["etag"]
if item.etag != etag:
- raise EtebaseValidationError('wrong_etag', 'Wrong etag. Expected {} got {}'.format(item.etag, etag),
- status_code=status.HTTP_409_CONFLICT)
+ raise EtebaseValidationError(
+ "wrong_etag",
+ "Wrong etag. Expected {} got {}".format(item.etag, etag),
+ status_code=status.HTTP_409_CONFLICT,
+ )
return data
@@ -262,49 +273,47 @@ class CollectionItemBulkGetSerializer(BetterErrorsMixin, serializers.ModelSerial
class Meta:
model = models.CollectionItem
- fields = ('uid', 'etag')
+ fields = ("uid", "etag")
class CollectionListMultiSerializer(BetterErrorsMixin, serializers.Serializer):
- collectionTypes = serializers.ListField(
- child=BinaryBase64Field()
- )
+ collectionTypes = serializers.ListField(child=BinaryBase64Field())
class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer):
collectionKey = CollectionEncryptionKeyField()
collectionType = CollectionTypeField()
- accessLevel = serializers.SerializerMethodField('get_access_level_from_context')
+ accessLevel = serializers.SerializerMethodField("get_access_level_from_context")
stoken = serializers.CharField(read_only=True)
- item = CollectionItemSerializer(many=False, source='main_item')
+ item = CollectionItemSerializer(many=False, source="main_item")
class Meta:
model = models.Collection
- fields = ('item', 'accessLevel', 'collectionKey', 'collectionType', 'stoken')
+ fields = ("item", "accessLevel", "collectionKey", "collectionType", "stoken")
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:
return obj.members.get(user=request.user).accessLevel
return None
def create(self, validated_data):
"""Function that's called when this serializer creates an item"""
- collection_key = validated_data.pop('collectionKey')
- collection_type = validated_data.pop('collectionType')
+ collection_key = validated_data.pop("collectionKey")
+ collection_type = validated_data.pop("collectionType")
- user = validated_data.get('owner')
- main_item_data = validated_data.pop('main_item')
- etag = main_item_data.pop('etag')
- revision_data = main_item_data.pop('content')
+ user = validated_data.get("owner")
+ main_item_data = validated_data.pop("main_item")
+ etag = main_item_data.pop("etag")
+ revision_data = main_item_data.pop("content")
instance = self.__class__.Meta.model(**validated_data)
with transaction.atomic():
_ = self.__class__.Meta.model.objects.select_for_update().filter(owner=user)
if etag is not None:
- raise EtebaseValidationError('bad_etag', 'etag is not null')
+ raise EtebaseValidationError("bad_etag", "etag is not null")
instance.save()
main_item = models.CollectionItem.objects.create(**main_item_data, collection=instance)
@@ -318,13 +327,14 @@ class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer):
collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=collection_type, owner=user)
- models.CollectionMember(collection=instance,
- stoken=models.Stoken.objects.create(),
- user=user,
- accessLevel=models.AccessLevels.ADMIN,
- encryptionKey=collection_key,
- collectionType=collection_type_obj,
- ).save()
+ models.CollectionMember(
+ collection=instance,
+ stoken=models.Stoken.objects.create(),
+ user=user,
+ accessLevel=models.AccessLevels.ADMIN,
+ encryptionKey=collection_key,
+ collectionType=collection_type_obj,
+ ).save()
return instance
@@ -333,15 +343,11 @@ class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer):
class CollectionMemberSerializer(BetterErrorsMixin, serializers.ModelSerializer):
- username = UserSlugRelatedField(
- source='user',
- read_only=True,
- style={'base_template': 'input.html'},
- )
+ username = UserSlugRelatedField(source="user", read_only=True, style={"base_template": "input.html"},)
class Meta:
model = models.CollectionMember
- fields = ('username', 'accessLevel')
+ fields = ("username", "accessLevel")
def create(self, validated_data):
raise NotImplementedError()
@@ -349,7 +355,7 @@ class CollectionMemberSerializer(BetterErrorsMixin, serializers.ModelSerializer)
def update(self, instance, validated_data):
with transaction.atomic():
# We only allow updating accessLevel
- access_level = validated_data.pop('accessLevel')
+ access_level = validated_data.pop("accessLevel")
if instance.accessLevel != access_level:
instance.stoken = models.Stoken.objects.create()
instance.accessLevel = access_level
@@ -359,31 +365,35 @@ class CollectionMemberSerializer(BetterErrorsMixin, serializers.ModelSerializer)
class CollectionInvitationSerializer(BetterErrorsMixin, serializers.ModelSerializer):
- username = UserSlugRelatedField(
- source='user',
- queryset=User.objects,
- style={'base_template': 'input.html'},
- )
- 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)
+ username = UserSlugRelatedField(source="user", queryset=User.objects, style={"base_template": "input.html"},)
+ 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()
class Meta:
model = models.CollectionInvitation
- fields = ('username', 'uid', 'collection', 'signedEncryptionKey', 'accessLevel',
- 'fromUsername', 'fromPubkey', 'version')
+ fields = (
+ "username",
+ "uid",
+ "collection",
+ "signedEncryptionKey",
+ "accessLevel",
+ "fromUsername",
+ "fromPubkey",
+ "version",
+ )
def validate_user(self, value):
- request = self.context['request']
+ request = self.context["request"]
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
def create(self, validated_data):
- request = self.context['request']
- collection = validated_data.pop('collection')
+ request = self.context["request"]
+ collection = validated_data.pop("collection")
member = collection.members.get(user=request.user)
@@ -391,12 +401,12 @@ class CollectionInvitationSerializer(BetterErrorsMixin, serializers.ModelSeriali
try:
return type(self).Meta.model.objects.create(**validated_data, fromMember=member)
except IntegrityError:
- raise EtebaseValidationError('invitation_exists', 'Invitation already exists')
+ raise EtebaseValidationError("invitation_exists", "Invitation already exists")
def update(self, instance, validated_data):
with transaction.atomic():
- instance.accessLevel = validated_data.pop('accessLevel')
- instance.signedEncryptionKey = validated_data.pop('signedEncryptionKey')
+ instance.accessLevel = validated_data.pop("accessLevel")
+ instance.signedEncryptionKey = validated_data.pop("signedEncryptionKey")
instance.save()
return instance
@@ -409,9 +419,9 @@ class InvitationAcceptSerializer(BetterErrorsMixin, serializers.Serializer):
def create(self, validated_data):
with transaction.atomic():
- invitation = self.context['invitation']
- encryption_key = validated_data.get('encryptionKey')
- collection_type = validated_data.pop('collectionType')
+ invitation = self.context["invitation"]
+ encryption_key = validated_data.get("encryptionKey")
+ collection_type = validated_data.pop("collectionType")
user = invitation.user
collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=collection_type, owner=user)
@@ -423,10 +433,11 @@ class InvitationAcceptSerializer(BetterErrorsMixin, serializers.Serializer):
accessLevel=invitation.accessLevel,
encryptionKey=encryption_key,
collectionType=collection_type_obj,
- )
+ )
models.CollectionMemberRemoved.objects.filter(
- user=invitation.user, collection=invitation.collection).delete()
+ user=invitation.user, collection=invitation.collection
+ ).delete()
invitation.delete()
@@ -437,12 +448,12 @@ class InvitationAcceptSerializer(BetterErrorsMixin, serializers.Serializer):
class UserSerializer(BetterErrorsMixin, serializers.ModelSerializer):
- pubkey = BinaryBase64Field(source='userinfo.pubkey')
- encryptedContent = BinaryBase64Field(source='userinfo.encryptedContent')
+ pubkey = BinaryBase64Field(source="userinfo.pubkey")
+ encryptedContent = BinaryBase64Field(source="userinfo.encryptedContent")
class Meta:
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):
@@ -450,7 +461,7 @@ class UserInfoPubkeySerializer(BetterErrorsMixin, serializers.ModelSerializer):
class Meta:
model = models.UserInfo
- fields = ('pubkey', )
+ fields = ("pubkey",)
class UserSignupSerializer(BetterErrorsMixin, serializers.ModelSerializer):
@@ -458,7 +469,7 @@ class UserSignupSerializer(BetterErrorsMixin, serializers.ModelSerializer):
model = User
fields = (User.USERNAME_FIELD, User.EMAIL_FIELD)
extra_kwargs = {
- 'username': {'validators': []}, # We specifically validate in SignupSerializer
+ "username": {"validators": []}, # We specifically validate in SignupSerializer
}
@@ -466,6 +477,7 @@ class AuthenticationSignupSerializer(BetterErrorsMixin, serializers.Serializer):
"""Used both for creating new accounts and setting up existing ones for the first time.
When setting up existing ones the email is ignored."
"""
+
user = UserSignupSerializer(many=False)
salt = BinaryBase64Field()
loginPubkey = BinaryBase64Field()
@@ -474,27 +486,27 @@ class AuthenticationSignupSerializer(BetterErrorsMixin, serializers.Serializer):
def create(self, validated_data):
"""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():
try:
- view = self.context.get('view', None)
+ view = self.context.get("view", None)
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:
# Create the user and save the casing the user chose as the first name
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()
except EtebaseValidationError as e:
raise e
except django_exceptions.ValidationError as e:
self.transform_validation_error("user", e)
except Exception as e:
- raise EtebaseValidationError('generic', str(e))
+ raise EtebaseValidationError("generic", str(e))
- if hasattr(instance, 'userinfo'):
- raise EtebaseValidationError('user_exists', 'User already exists', status_code=status.HTTP_409_CONFLICT)
+ if hasattr(instance, "userinfo"):
+ raise EtebaseValidationError("user_exists", "User already exists", status_code=status.HTTP_409_CONFLICT)
models.UserInfo.objects.create(**validated_data, owner=instance)
@@ -543,15 +555,15 @@ class AuthenticationChangePasswordInnerSerializer(AuthenticationLoginInnerSerial
class Meta:
model = models.UserInfo
- fields = ('loginPubkey', 'encryptedContent')
+ fields = ("loginPubkey", "encryptedContent")
def create(self, validated_data):
raise NotImplementedError()
def update(self, instance, validated_data):
with transaction.atomic():
- instance.loginPubkey = validated_data.pop('loginPubkey')
- instance.encryptedContent = validated_data.pop('encryptedContent')
+ instance.loginPubkey = validated_data.pop("loginPubkey")
+ instance.encryptedContent = validated_data.pop("encryptedContent")
instance.save()
return instance
diff --git a/django_etebase/signals.py b/django_etebase/signals.py
index 03dbed5..0fc3e80 100644
--- a/django_etebase/signals.py
+++ b/django_etebase/signals.py
@@ -1,3 +1,3 @@
from django.dispatch import Signal
-user_signed_up = Signal(providing_args=['request', 'user'])
+user_signed_up = Signal(providing_args=["request", "user"])
diff --git a/django_etebase/token_auth/apps.py b/django_etebase/token_auth/apps.py
index 118b872..a0e98be 100644
--- a/django_etebase/token_auth/apps.py
+++ b/django_etebase/token_auth/apps.py
@@ -2,4 +2,4 @@ from django.apps import AppConfig
class TokenAuthConfig(AppConfig):
- name = 'django_etebase.token_auth'
+ name = "django_etebase.token_auth"
diff --git a/django_etebase/token_auth/authentication.py b/django_etebase/token_auth/authentication.py
index 432c8cf..7e84956 100644
--- a/django_etebase/token_auth/authentication.py
+++ b/django_etebase/token_auth/authentication.py
@@ -12,19 +12,19 @@ MIN_REFRESH_INTERVAL = 60
class TokenAuthentication(DRFTokenAuthentication):
- keyword = 'Token'
+ keyword = "Token"
model = AuthToken
def authenticate_credentials(self, key):
- msg = _('Invalid token.')
+ msg = _("Invalid token.")
model = self.get_model()
try:
- token = model.objects.select_related('user').get(key=key)
+ token = model.objects.select_related("user").get(key=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed(msg)
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 < timezone.now():
@@ -43,4 +43,4 @@ class TokenAuthentication(DRFTokenAuthentication):
delta = (new_expiry - current_expiry).total_seconds()
if delta > MIN_REFRESH_INTERVAL:
auth_token.expiry = new_expiry
- auth_token.save(update_fields=('expiry',))
+ auth_token.save(update_fields=("expiry",))
diff --git a/django_etebase/token_auth/migrations/0001_initial.py b/django_etebase/token_auth/migrations/0001_initial.py
index 5a47366..660b38c 100644
--- a/django_etebase/token_auth/migrations/0001_initial.py
+++ b/django_etebase/token_auth/migrations/0001_initial.py
@@ -16,13 +16,23 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
- name='AuthToken',
+ name="AuthToken",
fields=[
- ('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)),
- ('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)),
+ ("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)),
+ ("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,
+ ),
+ ),
],
),
]
diff --git a/django_etebase/token_auth/models.py b/django_etebase/token_auth/models.py
index 0fe4766..ac1efff 100644
--- a/django_etebase/token_auth/models.py
+++ b/django_etebase/token_auth/models.py
@@ -17,10 +17,9 @@ def get_default_expiry():
class AuthToken(models.Model):
key = models.CharField(max_length=40, unique=True, db_index=True, default=generate_key)
- user = models.ForeignKey(User, null=False, blank=False,
- related_name='auth_token_set', on_delete=models.CASCADE)
+ user = models.ForeignKey(User, null=False, blank=False, related_name="auth_token_set", on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
expiry = models.DateTimeField(null=True, blank=True, default=get_default_expiry)
def __str__(self):
- return '{}: {}'.format(self.key, self.user)
+ return "{}: {}".format(self.key, self.user)
diff --git a/django_etebase/urls.py b/django_etebase/urls.py
index f6d982e..01797c1 100644
--- a/django_etebase/urls.py
+++ b/django_etebase/urls.py
@@ -7,24 +7,24 @@ from rest_framework_nested import routers
from django_etebase import views
router = routers.DefaultRouter()
-router.register(r'collection', views.CollectionViewSet)
-router.register(r'authentication', views.AuthenticationViewSet, basename='authentication')
-router.register(r'invitation/incoming', views.InvitationIncomingViewSet, basename='invitation_incoming')
-router.register(r'invitation/outgoing', views.InvitationOutgoingViewSet, basename='invitation_outgoing')
+router.register(r"collection", views.CollectionViewSet)
+router.register(r"authentication", views.AuthenticationViewSet, basename="authentication")
+router.register(r"invitation/incoming", views.InvitationIncomingViewSet, basename="invitation_incoming")
+router.register(r"invitation/outgoing", views.InvitationOutgoingViewSet, basename="invitation_outgoing")
-collections_router = routers.NestedSimpleRouter(router, r'collection', lookup='collection')
-collections_router.register(r'item', views.CollectionItemViewSet, basename='collection_item')
-collections_router.register(r'member', views.CollectionMemberViewSet, basename='collection_member')
+collections_router = routers.NestedSimpleRouter(router, r"collection", lookup="collection")
+collections_router.register(r"item", views.CollectionItemViewSet, basename="collection_item")
+collections_router.register(r"member", views.CollectionMemberViewSet, basename="collection_member")
-item_router = routers.NestedSimpleRouter(collections_router, r'item', lookup='collection_item')
-item_router.register(r'chunk', views.CollectionItemChunkViewSet, basename='collection_items_chunk')
+item_router = routers.NestedSimpleRouter(collections_router, r"item", lookup="collection_item")
+item_router.register(r"chunk", views.CollectionItemChunkViewSet, basename="collection_items_chunk")
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 = [
- path('v1/', include(router.urls)),
- path('v1/', include(collections_router.urls)),
- path('v1/', include(item_router.urls)),
+ path("v1/", include(router.urls)),
+ path("v1/", include(collections_router.urls)),
+ path("v1/", include(item_router.urls)),
]
diff --git a/django_etebase/utils.py b/django_etebase/utils.py
index 1351f9b..e496a77 100644
--- a/django_etebase/utils.py
+++ b/django_etebase/utils.py
@@ -18,9 +18,9 @@ def create_user(*args, **kwargs):
custom_func = app_settings.CREATE_USER_FUNC
if custom_func is not None:
return custom_func(*args, **kwargs)
- _ = kwargs.pop('view')
+ _ = kwargs.pop("view")
return User.objects.create_user(*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.")
diff --git a/django_etebase/views.py b/django_etebase/views.py
index 2dc7adf..7dd7526 100644
--- a/django_etebase/views.py
+++ b/django_etebase/views.py
@@ -45,34 +45,34 @@ from .drf_msgpack.renderers import MessagePackRenderer
from . import app_settings, permissions
from .renderers import JSONRenderer
from .models import (
- Collection,
- CollectionItem,
- CollectionItemRevision,
- CollectionMember,
- CollectionMemberRemoved,
- CollectionInvitation,
- Stoken,
- UserInfo,
- )
+ Collection,
+ CollectionItem,
+ CollectionItemRevision,
+ CollectionMember,
+ CollectionMemberRemoved,
+ CollectionInvitation,
+ Stoken,
+ UserInfo,
+)
from .serializers import (
- AuthenticationChangePasswordInnerSerializer,
- AuthenticationSignupSerializer,
- AuthenticationLoginChallengeSerializer,
- AuthenticationLoginSerializer,
- AuthenticationLoginInnerSerializer,
- CollectionSerializer,
- CollectionItemSerializer,
- CollectionItemBulkGetSerializer,
- CollectionItemDepSerializer,
- CollectionItemRevisionSerializer,
- CollectionItemChunkSerializer,
- CollectionListMultiSerializer,
- CollectionMemberSerializer,
- CollectionInvitationSerializer,
- InvitationAcceptSerializer,
- UserInfoPubkeySerializer,
- UserSerializer,
- )
+ AuthenticationChangePasswordInnerSerializer,
+ AuthenticationSignupSerializer,
+ AuthenticationLoginChallengeSerializer,
+ AuthenticationLoginSerializer,
+ AuthenticationLoginInnerSerializer,
+ CollectionSerializer,
+ CollectionItemSerializer,
+ CollectionItemBulkGetSerializer,
+ CollectionItemDepSerializer,
+ CollectionItemRevisionSerializer,
+ CollectionItemChunkSerializer,
+ CollectionListMultiSerializer,
+ CollectionMemberSerializer,
+ CollectionInvitationSerializer,
+ InvitationAcceptSerializer,
+ UserInfoPubkeySerializer,
+ UserSerializer,
+)
from .utils import get_user_queryset
from .exceptions import EtebaseValidationError
from .parsers import ChunkUploadParser
@@ -99,8 +99,8 @@ class BaseViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
serializer_class = self.serializer_class
- if self.request.method == 'PUT':
- serializer_class = getattr(self, 'serializer_update_class', serializer_class)
+ if self.request.method == "PUT":
+ serializer_class = getattr(self, "serializer_update_class", serializer_class)
return serializer_class
@@ -109,7 +109,7 @@ class BaseViewSet(viewsets.ModelViewSet):
return queryset.filter(members__user=user)
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):
stoken = self.get_stoken_obj_id(request)
@@ -118,7 +118,7 @@ class BaseViewSet(viewsets.ModelViewSet):
try:
return Stoken.objects.get(uid=stoken)
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
@@ -127,7 +127,7 @@ class BaseViewSet(viewsets.ModelViewSet):
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]
- 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:
queryset = queryset.filter(max_stoken__gt=stoken_rev.id)
@@ -137,18 +137,18 @@ class BaseViewSet(viewsets.ModelViewSet):
def get_queryset_stoken(self, queryset):
maxid = -1
for row in queryset:
- rowmaxid = getattr(row, 'max_stoken') or -1
+ rowmaxid = getattr(row, "max_stoken") or -1
maxid = max(maxid, rowmaxid)
new_stoken = (maxid >= 0) and Stoken.objects.get(id=maxid)
return new_stoken or None
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)
- result = list(queryset[:limit + 1])
+ result = list(queryset[: limit + 1])
if len(result) < limit + 1:
done = True
else:
@@ -165,21 +165,21 @@ class BaseViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(queryset, many=True)
ret = {
- 'data': serializer.data,
- 'done': True, # we always return all the items, so it's always done
+ "data": serializer.data,
+ "done": True, # we always return all the items, so it's always done
}
return Response(ret)
class CollectionViewSet(BaseViewSet):
- allowed_methods = ['GET', 'POST']
- permission_classes = BaseViewSet.permission_classes + (permissions.IsCollectionAdminOrReadOnly, )
+ allowed_methods = ["GET", "POST"]
+ permission_classes = BaseViewSet.permission_classes + (permissions.IsCollectionAdminOrReadOnly,)
queryset = Collection.objects.all()
serializer_class = CollectionSerializer
- lookup_field = 'main_item__uid'
- lookup_url_kwarg = 'uid'
- stoken_id_fields = ['items__revisions__stoken__id', 'members__stoken__id']
+ lookup_field = "main_item__uid"
+ lookup_url_kwarg = "uid"
+ stoken_id_fields = ["items__revisions__stoken__id", "members__stoken__id"]
def get_queryset(self, queryset=None):
if queryset is None:
@@ -188,8 +188,8 @@ class CollectionViewSet(BaseViewSet):
def get_serializer_context(self):
context = super().get_serializer_context()
- prefetch = self.request.query_params.get('prefetch', 'auto')
- context.update({'request': self.request, 'prefetch': prefetch})
+ prefetch = self.request.query_params.get("prefetch", "auto")
+ context.update({"request": self.request, "prefetch": prefetch})
return context
def destroy(self, request, uid=None, *args, **kwargs):
@@ -213,17 +213,18 @@ class CollectionViewSet(BaseViewSet):
queryset = self.get_queryset()
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):
serializer = CollectionListMultiSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
- collection_types = serializer.validated_data['collectionTypes']
+ collection_types = serializer.validated_data["collectionTypes"]
queryset = self.get_queryset()
# FIXME: Remove the isnull part once we attach collection types to all objects ("collection-type-migration")
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)
@@ -234,51 +235,50 @@ class CollectionViewSet(BaseViewSet):
serializer = self.get_serializer(result, many=True)
ret = {
- 'data': serializer.data,
- 'stoken': new_stoken,
- 'done': done,
+ "data": serializer.data,
+ "stoken": new_stoken,
+ "done": done,
}
stoken_obj = self.get_stoken_obj(request)
if stoken_obj is not None:
# 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)
- 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
# can point to the most recent collection change rather than most recent removed membership.
remed_qs = remed_qs.filter(stoken__id__lte=new_stoken_obj.id)
- remed = remed_qs.values_list('collection__main_item__uid', flat=True)
+ remed = remed_qs.values_list("collection__main_item__uid", flat=True)
if len(remed) > 0:
- ret['removedMemberships'] = [{'uid': x} for x in remed]
+ ret["removedMemberships"] = [{"uid": x} for x in remed]
return Response(ret)
class CollectionItemViewSet(BaseViewSet):
- allowed_methods = ['GET', 'POST', 'PUT']
- permission_classes = BaseViewSet.permission_classes + (permissions.HasWriteAccessOrReadOnly, )
+ allowed_methods = ["GET", "POST", "PUT"]
+ permission_classes = BaseViewSet.permission_classes + (permissions.HasWriteAccessOrReadOnly,)
queryset = CollectionItem.objects.all()
serializer_class = CollectionItemSerializer
- lookup_field = 'uid'
- stoken_id_fields = ['revisions__stoken__id']
+ lookup_field = "uid"
+ stoken_id_fields = ["revisions__stoken__id"]
def get_queryset(self):
- collection_uid = self.kwargs['collection_uid']
+ collection_uid = self.kwargs["collection_uid"]
try:
collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid)
except Collection.DoesNotExist:
raise Http404("Collection does not exist")
# XXX Potentially add this for performance: .prefetch_related('revisions__chunks')
- queryset = type(self).queryset.filter(collection__pk=collection.pk,
- revisions__current=True)
+ queryset = type(self).queryset.filter(collection__pk=collection.pk, revisions__current=True)
return queryset
def get_serializer_context(self):
context = super().get_serializer_context()
- prefetch = self.request.query_params.get('prefetch', 'auto')
- context.update({'request': self.request, 'prefetch': prefetch})
+ prefetch = self.request.query_params.get("prefetch", "auto")
+ context.update({"request": self.request, "prefetch": prefetch})
return context
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):
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)
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)
ret = {
- 'data': serializer.data,
- 'stoken': new_stoken,
- 'done': done,
+ "data": serializer.data,
+ "stoken": new_stoken,
+ "done": done,
}
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):
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)
- limit = int(request.GET.get('limit', 50))
- iterator = request.GET.get('iterator', None)
+ limit = int(request.GET.get("limit", 50))
+ iterator = request.GET.get("iterator", None)
- queryset = item.revisions.order_by('-id')
+ queryset = item.revisions.order_by("-id")
if iterator is not None:
iterator = get_object_or_404(queryset, uid=iterator)
queryset = queryset.filter(id__lt=iterator.id)
- result = list(queryset[:limit + 1])
+ result = list(queryset[: limit + 1])
if len(result) < limit + 1:
done = True
else:
@@ -336,17 +336,17 @@ class CollectionItemViewSet(BaseViewSet):
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 = {
- 'data': serializer.data,
- 'iterator': iterator,
- 'done': done,
+ "data": serializer.data,
+ "iterator": iterator,
+ "done": done,
}
return Response(ret)
# 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):
queryset = self.get_queryset()
@@ -356,79 +356,76 @@ class CollectionItemViewSet(BaseViewSet):
item_limit = 200
if len(serializer.validated_data) > item_limit:
- content = {'code': 'too_many_items',
- 'detail': 'Request has too many items. Limit: {}'. format(item_limit)}
+ content = {"code": "too_many_items", "detail": "Request has too many items. Limit: {}".format(item_limit)}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
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)
queryset = queryset.filter(uid__in=uids).exclude(revisions__in=revs)
new_stoken_obj = self.get_queryset_stoken(queryset)
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
serializer = self.get_serializer(queryset, many=True)
ret = {
- 'data': serializer.data,
- 'stoken': new_stoken,
- 'done': True, # we always return all the items, so it's always done
+ "data": serializer.data,
+ "stoken": new_stoken,
+ "done": True, # we always return all the items, so it's always done
}
return Response(ret)
- @action_decorator(detail=False, methods=['POST'])
+ @action_decorator(detail=False, methods=["POST"])
def batch(self, request, collection_uid=None, *args, **kwargs):
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):
- stoken = request.GET.get('stoken', None)
+ stoken = request.GET.get("stoken", None)
with transaction.atomic(): # We need this for locking on the collection object
collection_object = get_object_or_404(
self.get_collection_queryset(Collection.objects).select_for_update(), # Lock writes on the collection
- main_item__uid=collection_uid)
+ main_item__uid=collection_uid,
+ )
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)
- items = request.data.get('items')
- deps = request.data.get('deps', None)
+ items = request.data.get("items")
+ deps = request.data.get("deps", None)
# FIXME: It should just be one serializer
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)
deps_serializer = CollectionItemDepSerializer(data=deps, context=context, many=True)
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:
items = serializer.save(collection=collection_object)
- ret = {
- }
+ ret = {}
return Response(ret, status=status.HTTP_200_OK)
return Response(
- {
- "items": serializer.errors,
- "deps": deps_serializer.errors if deps is not None else [],
- },
- status=status.HTTP_409_CONFLICT)
+ {"items": serializer.errors, "deps": deps_serializer.errors if deps is not None else [],},
+ status=status.HTTP_409_CONFLICT,
+ )
class CollectionItemChunkViewSet(viewsets.ViewSet):
- allowed_methods = ['GET', 'PUT']
+ allowed_methods = ["GET", "PUT"]
authentication_classes = BaseViewSet.authentication_classes
permission_classes = BaseViewSet.permission_classes
renderer_classes = BaseViewSet.renderer_classes
- parser_classes = (ChunkUploadParser, )
+ parser_classes = (ChunkUploadParser,)
serializer_class = CollectionItemChunkSerializer
- lookup_field = 'uid'
+ lookup_field = "uid"
def get_serializer_class(self):
return self.serializer_class
@@ -452,13 +449,12 @@ class CollectionItemChunkViewSet(viewsets.ViewSet):
serializer.save(collection=col)
except IntegrityError:
return Response(
- {"code": "chunk_exists", "detail": "Chunk already exists."},
- status=status.HTTP_409_CONFLICT
+ {"code": "chunk_exists", "detail": "Chunk already exists."}, status=status.HTTP_409_CONFLICT
)
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):
import os
from django.views.static import serve
@@ -476,24 +472,24 @@ class CollectionItemChunkViewSet(viewsets.ViewSet):
class CollectionMemberViewSet(BaseViewSet):
- allowed_methods = ['GET', 'PUT', 'DELETE']
+ allowed_methods = ["GET", "PUT", "DELETE"]
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()
serializer_class = CollectionMemberSerializer
- lookup_field = f'user__{User.USERNAME_FIELD}__iexact'
- lookup_url_kwarg = 'username'
- stoken_id_fields = ['stoken__id']
+ lookup_field = f"user__{User.USERNAME_FIELD}__iexact"
+ lookup_url_kwarg = "username"
+ 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
# (if we want to transfer, we need to do that specifically)
def get_queryset(self, queryset=None):
- collection_uid = self.kwargs['collection_uid']
+ collection_uid = self.kwargs["collection_uid"]
try:
collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid)
except Collection.DoesNotExist:
- raise Http404('Collection does not exist')
+ raise Http404("Collection does not exist")
if queryset is None:
queryset = type(self).queryset
@@ -502,18 +498,18 @@ class CollectionMemberViewSet(BaseViewSet):
# We override this method because we expect the stoken to be called iterator
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):
- 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)
new_stoken = new_stoken_obj and new_stoken_obj.uid
serializer = self.get_serializer(result, many=True)
ret = {
- 'data': serializer.data,
- 'iterator': new_stoken, # Here we call it an iterator, it's only stoken for collection/items
- 'done': done,
+ "data": serializer.data,
+ "iterator": new_stoken, # Here we call it an iterator, it's only stoken for collection/items
+ "done": done,
}
return Response(ret)
@@ -526,9 +522,9 @@ class CollectionMemberViewSet(BaseViewSet):
def perform_destroy(self, instance):
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):
- 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)
member = col.members.get(user=request.user)
@@ -540,20 +536,20 @@ class CollectionMemberViewSet(BaseViewSet):
class InvitationBaseViewSet(BaseViewSet):
queryset = CollectionInvitation.objects.all()
serializer_class = CollectionInvitationSerializer
- lookup_field = 'uid'
- lookup_url_kwarg = 'invitation_uid'
+ lookup_field = "uid"
+ lookup_url_kwarg = "invitation_uid"
def list(self, request, collection_uid=None, *args, **kwargs):
- limit = int(request.GET.get('limit', 50))
- iterator = request.GET.get('iterator', None)
+ limit = int(request.GET.get("limit", 50))
+ 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:
iterator = get_object_or_404(queryset, uid=iterator)
queryset = queryset.filter(id__gt=iterator.id)
- result = list(queryset[:limit + 1])
+ result = list(queryset[: limit + 1])
if len(result) < limit + 1:
done = True
else:
@@ -562,19 +558,19 @@ class InvitationBaseViewSet(BaseViewSet):
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 = {
- 'data': serializer.data,
- 'iterator': iterator,
- 'done': done,
+ "data": serializer.data,
+ "iterator": iterator,
+ "done": done,
}
return Response(ret)
class InvitationOutgoingViewSet(InvitationBaseViewSet):
- allowed_methods = ['GET', 'POST', 'PUT', 'DELETE']
+ allowed_methods = ["GET", "POST", "PUT", "DELETE"]
def get_queryset(self, queryset=None):
if queryset is None:
@@ -585,28 +581,29 @@ class InvitationOutgoingViewSet(InvitationBaseViewSet):
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
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:
collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid)
except Collection.DoesNotExist:
- raise Http404('Collection does not exist')
+ raise Http404("Collection does not exist")
- if request.user == serializer.validated_data.get('user'):
- content = {'code': 'self_invite', 'detail': 'Inviting yourself is invalid'}
+ if request.user == serializer.validated_data.get("user"):
+ content = {"code": "self_invite", "detail": "Inviting yourself is invalid"}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
if not permissions.is_collection_admin(collection, request.user):
- raise PermissionDenied({'code': 'admin_access_required',
- 'detail': 'User is not an admin of this collection'})
+ raise PermissionDenied(
+ {"code": "admin_access_required", "detail": "User is not an admin of this collection"}
+ )
serializer.save(collection=collection)
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):
- username = request.GET.get('username')
+ username = request.GET.get("username")
kwargs = {User.USERNAME_FIELD: username.lower()}
user = get_object_or_404(get_user_queryset(User.objects.all(), self), **kwargs)
user_info = get_object_or_404(UserInfo.objects.all(), owner=user)
@@ -615,7 +612,7 @@ class InvitationOutgoingViewSet(InvitationBaseViewSet):
class InvitationIncomingViewSet(InvitationBaseViewSet):
- allowed_methods = ['GET', 'DELETE']
+ allowed_methods = ["GET", "DELETE"]
def get_queryset(self, queryset=None):
if queryset is None:
@@ -623,11 +620,11 @@ class InvitationIncomingViewSet(InvitationBaseViewSet):
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):
invitation = get_object_or_404(self.get_queryset(), uid=invitation_uid)
context = self.get_serializer_context()
- context.update({'invitation': invitation})
+ context.update({"invitation": invitation})
serializer = InvitationAcceptSerializer(data=request.data, context=context)
serializer.is_valid(raise_exception=True)
@@ -636,36 +633,37 @@ class InvitationIncomingViewSet(InvitationBaseViewSet):
class AuthenticationViewSet(viewsets.ViewSet):
- allowed_methods = ['POST']
+ allowed_methods = ["POST"]
authentication_classes = BaseViewSet.authentication_classes
renderer_classes = BaseViewSet.renderer_classes
parser_classes = BaseViewSet.parser_classes
def get_encryption_key(self, salt):
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',
- encoder=nacl.encoding.RawEncoder)
+ return nacl.hash.blake2b(
+ b"",
+ key=key,
+ salt=salt[: nacl.hash.BLAKE2B_SALTBYTES],
+ person=b"etebase-auth",
+ encoder=nacl.encoding.RawEncoder,
+ )
def get_queryset(self):
return get_user_queryset(User.objects.all(), self)
def get_serializer_context(self):
- return {
- 'request': self.request,
- 'format': self.format_kwarg,
- 'view': self
- }
+ return {"request": self.request, "format": self.format_kwarg, "view": self}
def login_response_data(self, user):
return {
- 'token': AuthToken.objects.create(user=user).key,
- 'user': UserSerializer(user).data,
+ "token": AuthToken.objects.create(user=user).key,
+ "user": UserSerializer(user).data,
}
def list(self, request, *args, **kwargs):
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):
serializer = AuthenticationSignupSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
@@ -677,23 +675,23 @@ class AuthenticationViewSet(viewsets.ViewSet):
return Response(data, status=status.HTTP_201_CREATED)
def get_login_user(self, username):
- kwargs = {User.USERNAME_FIELD + '__iexact': username.lower()}
+ kwargs = {User.USERNAME_FIELD + "__iexact": username.lower()}
try:
user = self.get_queryset().get(**kwargs)
- if not hasattr(user, 'userinfo'):
- raise AuthenticationFailed({'code': 'user_not_init', 'detail': 'User not properly init'})
+ if not hasattr(user, "userinfo"):
+ raise AuthenticationFailed({"code": "user_not_init", "detail": "User not properly init"})
return user
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):
from datetime import datetime
- username = validated_data.get('username')
+ username = validated_data.get("username")
user = self.get_login_user(username)
- host = validated_data['host']
- challenge = validated_data['challenge']
- action = validated_data['action']
+ host = validated_data["host"]
+ challenge = validated_data["challenge"]
+ action = validated_data["action"]
salt = bytes(user.userinfo.salt)
enc_key = self.get_encryption_key(salt)
@@ -702,17 +700,17 @@ class AuthenticationViewSet(viewsets.ViewSet):
challenge_data = msgpack_decode(box.decrypt(challenge))
now = int(datetime.now().timestamp())
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)
- elif now - challenge_data['timestamp'] > app_settings.CHALLENGE_VALID_SECONDS:
- content = {'code': 'challenge_expired', 'detail': 'Login challange has expired'}
+ elif now - challenge_data["timestamp"] > app_settings.CHALLENGE_VALID_SECONDS:
+ content = {"code": "challenge_expired", "detail": "Login challange has expired"}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
- elif challenge_data['userId'] != user.id:
- content = {'code': 'wrong_user', 'detail': 'This challenge is for the wrong user'}
+ elif challenge_data["userId"] != user.id:
+ content = {"code": "wrong_user", "detail": "This challenge is for the wrong user"}
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())
- content = {'code': 'wrong_host', 'detail': detail}
+ content = {"code": "wrong_host", "detail": detail}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
verify_key = nacl.signing.VerifyKey(bytes(user.userinfo.loginPubkey), encoder=nacl.encoding.RawEncoder)
@@ -720,22 +718,24 @@ class AuthenticationViewSet(viewsets.ViewSet):
try:
verify_key.verify(response_raw, signature)
except nacl.exceptions.BadSignatureError:
- return Response({'code': 'login_bad_signature', 'detail': 'Wrong password for user.'},
- status=status.HTTP_401_UNAUTHORIZED)
+ return Response(
+ {"code": "login_bad_signature", "detail": "Wrong password for user."},
+ status=status.HTTP_401_UNAUTHORIZED,
+ )
return None
- @action_decorator(detail=False, methods=['GET'])
+ @action_decorator(detail=False, methods=["GET"])
def is_etebase(self, request, *args, **kwargs):
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):
from datetime import datetime
serializer = AuthenticationLoginChallengeSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
- username = serializer.validated_data.get('username')
+ username = serializer.validated_data.get("username")
user = self.get_login_user(username)
salt = bytes(user.userinfo.salt)
@@ -755,25 +755,26 @@ class AuthenticationViewSet(viewsets.ViewSet):
}
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):
outer_serializer = AuthenticationLoginSerializer(data=request.data)
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)
- 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.is_valid(raise_exception=True)
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:
return bad_login_response
- username = serializer.validated_data.get('username')
+ username = serializer.validated_data.get("username")
user = self.get_login_user(username)
data = self.login_response_data(user)
@@ -782,27 +783,28 @@ class AuthenticationViewSet(viewsets.ViewSet):
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):
request.auth.delete()
user_logged_out.send(sender=request.user.__class__, request=request, user=request.user)
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):
outer_serializer = AuthenticationLoginSerializer(data=request.data)
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)
- 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.is_valid(raise_exception=True)
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:
return bad_login_response
@@ -810,35 +812,32 @@ class AuthenticationViewSet(viewsets.ViewSet):
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):
get_dashboard_url = app_settings.DASHBOARD_URL_FUNC
if get_dashboard_url is None:
- raise EtebaseValidationError('not_supported', 'This server doesn\'t have a user dashboard.',
- status_code=status.HTTP_400_BAD_REQUEST)
+ raise EtebaseValidationError(
+ "not_supported", "This server doesn't have a user dashboard.", status_code=status.HTTP_400_BAD_REQUEST
+ )
ret = {
- 'url': get_dashboard_url(request, *args, **kwargs),
+ "url": get_dashboard_url(request, *args, **kwargs),
}
return Response(ret)
class TestAuthenticationViewSet(viewsets.ViewSet):
- allowed_methods = ['POST']
+ allowed_methods = ["POST"]
renderer_classes = BaseViewSet.renderer_classes
parser_classes = BaseViewSet.parser_classes
def get_serializer_context(self):
- return {
- 'request': self.request,
- 'format': self.format_kwarg,
- 'view': self
- }
+ return {"request": self.request, "format": self.format_kwarg, "view": self}
def list(self, request, *args, **kwargs):
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):
# Only run when in DEBUG mode! It's only used for tests
if not settings.DEBUG:
@@ -846,13 +845,13 @@ class TestAuthenticationViewSet(viewsets.ViewSet):
with transaction.atomic():
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
- 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.")
- if hasattr(user, 'userinfo'):
+ if hasattr(user, "userinfo"):
user.userinfo.delete()
serializer = AuthenticationSignupSerializer(data=request.data, context=self.get_serializer_context())
diff --git a/etebase_server/asgi.py b/etebase_server/asgi.py
index 44f1c53..0bf63ec 100644
--- a/etebase_server/asgi.py
+++ b/etebase_server/asgi.py
@@ -11,6 +11,6 @@ import os
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()
diff --git a/etebase_server/settings.py b/etebase_server/settings.py
index e792856..6243df6 100644
--- a/etebase_server/settings.py
+++ b/etebase_server/settings.py
@@ -17,7 +17,7 @@ from .utils import get_secret_from_file
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
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
@@ -37,10 +37,9 @@ ALLOWED_HOSTS = []
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.environ.get('ETEBASE_DB_PATH',
- os.path.join(BASE_DIR, 'db.sqlite3')),
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": os.environ.get("ETEBASE_DB_PATH", os.path.join(BASE_DIR, "db.sqlite3")),
}
}
@@ -48,78 +47,68 @@ DATABASES = {
# Application definition
INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'corsheaders',
- 'rest_framework',
- 'myauth.apps.MyauthConfig',
- 'django_etebase.apps.DjangoEtebaseConfig',
- 'django_etebase.token_auth.apps.TokenAuthConfig',
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "corsheaders",
+ "rest_framework",
+ "myauth.apps.MyauthConfig",
+ "django_etebase.apps.DjangoEtebaseConfig",
+ "django_etebase.token_auth.apps.TokenAuthConfig",
]
MIDDLEWARE = [
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'corsheaders.middleware.CorsMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "corsheaders.middleware.CorsMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
-ROOT_URLCONF = 'etebase_server.urls'
+ROOT_URLCONF = "etebase_server.urls"
TEMPLATES = [
{
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [
- os.path.join(BASE_DIR, 'templates')
- ],
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [os.path.join(BASE_DIR, "templates")],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "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
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
- {
- '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.UserAttributeSimilarityValidator",},
+ {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},
+ {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},
+ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
]
# Internationalization
# 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
@@ -133,11 +122,11 @@ CORS_ORIGIN_ALLOW_ALL = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
-STATIC_URL = '/static/'
-STATIC_ROOT = os.environ.get('DJANGO_STATIC_ROOT', os.path.join(BASE_DIR, 'static'))
+STATIC_URL = "/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_URL = '/user-media/'
+MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT", os.path.join(BASE_DIR, "media"))
+MEDIA_URL = "/user-media/"
ETEBASE_API_PERMISSIONS = ['rest_framework.permissions.IsAuthenticated']
ETEBASE_API_AUTHENTICATORS = ('django_etebase.token_auth.authentication.TokenAuthentication',
@@ -145,28 +134,40 @@ ETEBASE_API_AUTHENTICATORS = ('django_etebase.token_auth.authentication.TokenAut
ETEBASE_CREATE_USER_FUNC = 'django_etebase.utils.create_user_blocked'
# Define where to find configuration files
-config_locations = ['etebase-server.ini', '/etc/etebase-server/etebase-server.ini']
+config_locations = [
+ os.environ.get("ETEBASE_EASY_CONFIG_PATH", ""),
+ "etebase-server.ini",
+ "/etc/etebase-server/etebase-server.ini",
+]
+
+ETEBASE_API_PERMISSIONS = ("rest_framework.permissions.IsAuthenticated",)
+ETEBASE_API_AUTHENTICATORS = (
+ "django_etebase.token_auth.authentication.TokenAuthentication",
+ "rest_framework.authentication.SessionAuthentication",
+)
+ETEBASE_CREATE_USER_FUNC = "django_etebase.utils.create_user_blocked"
+
# Use config file if present
if any(os.path.isfile(x) for x in config_locations):
config = configparser.ConfigParser()
config.read(config_locations)
- section = config['global']
+ section = config["global"]
- SECRET_FILE = section.get('secret_file', SECRET_FILE)
- STATIC_ROOT = section.get('static_root', STATIC_ROOT)
- STATIC_URL = section.get('static_url', STATIC_URL)
- MEDIA_ROOT = section.get('media_root', MEDIA_ROOT)
- MEDIA_URL = section.get('media_url', MEDIA_URL)
- LANGUAGE_CODE = section.get('language_code', LANGUAGE_CODE)
- TIME_ZONE = section.get('time_zone', TIME_ZONE)
- DEBUG = section.getboolean('debug', DEBUG)
+ SECRET_FILE = section.get("secret_file", SECRET_FILE)
+ STATIC_ROOT = section.get("static_root", STATIC_ROOT)
+ STATIC_URL = section.get("static_url", STATIC_URL)
+ MEDIA_ROOT = section.get("media_root", MEDIA_ROOT)
+ MEDIA_URL = section.get("media_url", MEDIA_URL)
+ LANGUAGE_CODE = section.get("language_code", LANGUAGE_CODE)
+ TIME_ZONE = section.get("time_zone", TIME_ZONE)
+ DEBUG = section.getboolean("debug", DEBUG)
- if 'allowed_hosts' in config:
- ALLOWED_HOSTS = [y for x, y in config.items('allowed_hosts')]
+ if "allowed_hosts" in config:
+ ALLOWED_HOSTS = [y for x, y in config.items("allowed_hosts")]
- if 'database' in config:
- DATABASES = { 'default': { x.upper(): y for x, y in config.items('database') } }
+ if "database" in config:
+ DATABASES = {"default": {x.upper(): y for x, y in config.items("database")}}
if 'ldap' in config:
ldap = config['ldap']
@@ -186,5 +187,5 @@ try:
except ImportError:
pass
-if 'SECRET_KEY' not in locals():
+if "SECRET_KEY" not in locals():
SECRET_KEY = get_secret_from_file(SECRET_FILE)
diff --git a/etebase_server/urls.py b/etebase_server/urls.py
index fddc32f..f285977 100644
--- a/etebase_server/urls.py
+++ b/etebase_server/urls.py
@@ -5,13 +5,12 @@ from django.urls import path
from django.views.generic import TemplateView
urlpatterns = [
- url(r'^api/', include('django_etebase.urls')),
- url(r'^admin/', admin.site.urls),
-
- path('', TemplateView.as_view(template_name='success.html')),
+ url(r"^api/", include("django_etebase.urls")),
+ url(r"^admin/", admin.site.urls),
+ path("", TemplateView.as_view(template_name="success.html")),
]
if settings.DEBUG:
urlpatterns += [
- url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
+ url(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework")),
]
diff --git a/etebase_server/utils.py b/etebase_server/utils.py
index 21c99f2..64ed657 100644
--- a/etebase_server/utils.py
+++ b/etebase_server/utils.py
@@ -14,6 +14,7 @@
from django.core.management import utils
+
def get_secret_from_file(path):
try:
with open(path, "r") as f:
diff --git a/etebase_server/wsgi.py b/etebase_server/wsgi.py
index cf449a1..908f88c 100644
--- a/etebase_server/wsgi.py
+++ b/etebase_server/wsgi.py
@@ -11,6 +11,6 @@ import os
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()
diff --git a/manage.py b/manage.py
index b793fd2..91277fb 100755
--- a/manage.py
+++ b/manage.py
@@ -5,7 +5,7 @@ import sys
def main():
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'etebase_server.settings')
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "etebase_server.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
@@ -17,5 +17,5 @@ def main():
execute_from_command_line(sys.argv)
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/myauth/admin.py b/myauth/admin.py
index f91be8f..1f4b767 100644
--- a/myauth/admin.py
+++ b/myauth/admin.py
@@ -1,5 +1,12 @@
from django.contrib import admin
-from django.contrib.auth.admin import UserAdmin
+from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from .models import User
+from .forms import AdminUserCreationForm
+
+
+class UserAdmin(DjangoUserAdmin):
+ add_form = AdminUserCreationForm
+ add_fieldsets = ((None, {"classes": ("wide",), "fields": ("username",),}),)
+
admin.site.register(User, UserAdmin)
diff --git a/myauth/apps.py b/myauth/apps.py
index 611e83d..96cb29b 100644
--- a/myauth/apps.py
+++ b/myauth/apps.py
@@ -2,4 +2,4 @@ from django.apps import AppConfig
class MyauthConfig(AppConfig):
- name = 'myauth'
+ name = "myauth"
diff --git a/myauth/forms.py b/myauth/forms.py
new file mode 100644
index 0000000..7aacb9b
--- /dev/null
+++ b/myauth/forms.py
@@ -0,0 +1,29 @@
+from django import forms
+from django.contrib.auth import get_user_model
+from django.contrib.auth.forms import UsernameField
+
+User = get_user_model()
+
+
+class AdminUserCreationForm(forms.ModelForm):
+ """
+ A form that creates a user, with no privileges, from the given username and
+ password.
+ """
+
+ class Meta:
+ model = User
+ fields = ("username",)
+ field_classes = {"username": UsernameField}
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if self._meta.model.USERNAME_FIELD in self.fields:
+ self.fields[self._meta.model.USERNAME_FIELD].widget.attrs["autofocus"] = True
+
+ def save(self, commit=True):
+ user = super().save(commit=False)
+ user.set_unusable_password()
+ if commit:
+ user.save()
+ return user
diff --git a/myauth/migrations/0001_initial.py b/myauth/migrations/0001_initial.py
index 1f81e95..e6c2cba 100644
--- a/myauth/migrations/0001_initial.py
+++ b/myauth/migrations/0001_initial.py
@@ -11,34 +11,79 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ('auth', '0011_update_proxy_permissions'),
+ ("auth", "0011_update_proxy_permissions"),
]
operations = [
migrations.CreateModel(
- name='User',
+ name="User",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('password', models.CharField(max_length=128, verbose_name='password')),
- ('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')),
- ('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()),
+ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+ ("password", models.CharField(max_length=128, verbose_name="password")),
+ ("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",
+ ),
+ ),
+ ("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()),],
),
]
diff --git a/myauth/migrations/0002_auto_20200515_0801.py b/myauth/migrations/0002_auto_20200515_0801.py
index 3ce02b2..068c9ae 100644
--- a/myauth/migrations/0002_auto_20200515_0801.py
+++ b/myauth/migrations/0002_auto_20200515_0801.py
@@ -7,13 +7,20 @@ import myauth.models
class Migration(migrations.Migration):
dependencies = [
- ('myauth', '0001_initial'),
+ ("myauth", "0001_initial"),
]
operations = [
migrations.AlterField(
- model_name='user',
- 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'),
+ model_name="user",
+ 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",
+ ),
),
]
diff --git a/myauth/models.py b/myauth/models.py
index 611555b..d6585a8 100644
--- a/myauth/models.py
+++ b/myauth/models.py
@@ -7,17 +7,14 @@ from django.utils.translation import gettext_lazy as _
@deconstructible
class UnicodeUsernameValidator(validators.RegexValidator):
- regex = r'^[\w.-]+\Z'
- message = _(
- 'Enter a valid username. This value may contain only letters, '
- 'numbers, and ./-/_ characters.'
- )
+ regex = r"^[\w.-]+\Z"
+ message = _("Enter a valid username. This value may contain only letters, " "numbers, and ./-/_ characters.")
flags = 0
class UserManager(DjangoUserManager):
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):
@@ -26,14 +23,12 @@ class User(AbstractUser):
objects = UserManager()
username = models.CharField(
- _('username'),
+ _("username"),
max_length=150,
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],
- error_messages={
- 'unique': _("A user with that username already exists."),
- },
+ error_messages={"unique": _("A user with that username already exists."),},
)
@classmethod
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..e34796e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,2 @@
+[tool.black]
+line-length = 120
\ No newline at end of file
diff --git a/requirements.in/development.txt b/requirements.in/development.txt
index c752bfb..a956471 100644
--- a/requirements.in/development.txt
+++ b/requirements.in/development.txt
@@ -1,3 +1,4 @@
coverage
pip-tools
pywatchman
+black
\ No newline at end of file