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