Use black for code formatting and format the code

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

View File

@ -21,18 +21,19 @@ class AppSettings:
def import_from_str(self, name):
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_")

View File

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

View File

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

View File

@ -5,10 +5,10 @@ from rest_framework.exceptions import ParseError
class MessagePackParser(BaseParser):
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))

View File

@ -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)

View File

@ -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

View File

@ -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]")
],
options={
'unique_together': {('uid', 'owner')},
},
),
),
("version", models.PositiveSmallIntegerField()),
("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={"unique_together": {("uid", "owner")},},
),
migrations.CreateModel(
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]")
],
options={
'unique_together': {('uid', 'collection')},
},
),
),
("version", models.PositiveSmallIntegerField()),
("encryptionKey", models.BinaryField(editable=True, null=True)),
(
"collection",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="items",
to="django_etebase.Collection",
),
),
],
options={"unique_together": {("uid", "collection")},},
),
migrations.CreateModel(
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}$"
)
],
options={
'unique_together': {('item', 'current')},
},
),
),
("meta", models.BinaryField(editable=True)),
("current", models.BooleanField(db_index=True, default=True, null=True)),
("deleted", models.BooleanField(default=False)),
(
"item",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="revisions",
to="django_etebase.CollectionItem",
),
),
],
options={"unique_together": {("item", "current")},},
),
migrations.CreateModel(
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")},},
),
]

View File

@ -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)),
],
),
]

View File

@ -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}$"
)
],
options={
'unique_together': {('user', 'fromMember')},
},
),
),
("signedEncryptionKey", models.BinaryField()),
(
"accessLevel",
models.CharField(
choices=[("adm", "Admin"), ("rw", "Read Write"), ("ro", "Read Only")],
default="ro",
max_length=3,
),
),
(
"fromMember",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="django_etebase.CollectionMember"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="incoming_invitations",
to=settings.AUTH_USER_MODEL,
),
),
],
options={"unique_together": {("user", "fromMember")},},
),
]

View File

@ -6,13 +6,11 @@ from django.db import migrations, models
class Migration(migrations.Migration):
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),
),
]

View File

@ -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",),
]

View File

@ -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,
),
]

View File

@ -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}$"
)
],
),
),
]

View File

@ -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"
),
),
]

View File

@ -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 = [

View File

@ -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"),
),
]

View File

@ -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"
),
),
]

View File

@ -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 = [

View File

@ -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")},},
),
]

View File

@ -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",),
]

View File

@ -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),
),
]

View File

@ -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'),
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',
),
migrations.AlterUniqueTogether(name="collection", unique_together=set(),),
migrations.RemoveField(model_name="collection", name="uid",),
migrations.RemoveField(model_name="collection", name="version",),
]

View File

@ -8,18 +8,27 @@ import django.db.models.deletion
class Migration(migrations.Migration):
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]*$")],
),
),
]

View File

@ -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\\-_]*$")
],
),
),
]

View File

@ -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\\-_]*$")
],
),
),
]

View File

@ -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",),
]

View File

@ -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,}$")
],
),
),
]

View File

@ -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")},),
]

View File

@ -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",
),
),
]

View File

@ -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 = [

View File

@ -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'),
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',
),
migrations.AlterUniqueTogether(name="collectionitemchunk", unique_together={("collection", "uid")},),
migrations.RemoveField(model_name="collectionitemchunk", name="item",),
]

View File

@ -6,18 +6,10 @@ from django.db import migrations
class Migration(migrations.Migration):
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",),
]

View File

@ -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),
),
]

View File

@ -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 = [

View File

@ -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",),
]

View File

@ -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",
),
),
]

View File

@ -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"
),
),
]

View File

@ -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),
),
]

View File

@ -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(
stoken = (
Stoken.objects.filter(
Q(collectionitemrevision__item__collection=self) | Q(collectionmember__collection=self)
).order_by('id').last()
)
.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):

View File

@ -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]

View File

@ -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:

View File

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

View File

@ -29,7 +29,7 @@ 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
@ -42,11 +42,11 @@ def process_revisions_for_item(item, revision_data):
# If the chunk already exists we assume it's fine. Otherwise, we upload it.
if 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)
@ -60,7 +60,7 @@ def process_revisions_for_item(item, revision_data):
def b64encode(value):
return base64.urlsafe_b64encode(value).decode('ascii').strip('=')
return base64.urlsafe_b64encode(value).decode("ascii").strip("=")
def b64decode(data):
@ -85,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
@ -93,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
@ -102,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):
@ -115,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,)
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]))
@ -133,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):
@ -155,54 +149,50 @@ 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
"uid": {"validators": []}, # We deal with it in the serializers
}
@ -213,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
@ -229,12 +219,15 @@ class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer):
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'):
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
@ -260,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
@ -277,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)
@ -333,7 +327,8 @@ class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer):
collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=collection_type, owner=user)
models.CollectionMember(collection=instance,
models.CollectionMember(
collection=instance,
stoken=models.Stoken.objects.create(),
user=user,
accessLevel=models.AccessLevels.ADMIN,
@ -348,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()
@ -364,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
@ -374,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)
@ -406,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
@ -424,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)
@ -441,7 +436,8 @@ class InvitationAcceptSerializer(BetterErrorsMixin, serializers.Serializer):
)
models.CollectionMemberRemoved.objects.filter(
user=invitation.user, collection=invitation.collection).delete()
user=invitation.user, collection=invitation.collection
).delete()
invitation.delete()
@ -452,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):
@ -465,7 +461,7 @@ class UserInfoPubkeySerializer(BetterErrorsMixin, serializers.ModelSerializer):
class Meta:
model = models.UserInfo
fields = ('pubkey', )
fields = ("pubkey",)
class UserSignupSerializer(BetterErrorsMixin, serializers.ModelSerializer):
@ -473,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
}
@ -481,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()
@ -489,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)
@ -558,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

View File

@ -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"])

View File

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

View File

@ -12,19 +12,19 @@ MIN_REFRESH_INTERVAL = 60
class TokenAuthentication(DRFTokenAuthentication):
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",))

View File

@ -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,
),
),
],
),
]

View File

@ -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)

View File

@ -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)),
]

View File

@ -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.")

View File

@ -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,14 +137,14 @@ 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)
@ -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']
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']
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,21 +307,21 @@ 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)
@ -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,)
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,)
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,14 +536,14 @@ 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)
@ -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())

View File

@ -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()

View File

@ -17,7 +17,7 @@ from .utils import get_secret_from_file
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
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,18 +122,18 @@ 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/"
# Define where to find configuration files
config_locations = [
os.environ.get('ETEBASE_EASY_CONFIG_PATH', ''),
'etebase-server.ini',
'/etc/etebase-server/etebase-server.ini',
os.environ.get("ETEBASE_EASY_CONFIG_PATH", ""),
"etebase-server.ini",
"/etc/etebase-server/etebase-server.ini",
]
# Use config file if present
@ -152,27 +141,29 @@ 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")}}
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'
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"
# Make an `etebase_server_settings` module available to override settings.
try:
@ -180,5 +171,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)

View File

@ -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")),
]

View File

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

View File

@ -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()

View File

@ -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()

View File

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

View File

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

View File

@ -14,12 +14,12 @@ class AdminUserCreationForm(forms.ModelForm):
class Meta:
model = User
fields = ("username",)
field_classes = {'username': UsernameField}
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
self.fields[self._meta.model.USERNAME_FIELD].widget.attrs["autofocus"] = True
def save(self, commit=True):
user = super().save(commit=False)
@ -27,4 +27,3 @@ class AdminUserCreationForm(forms.ModelForm):
if commit:
user.save()
return user

View File

@ -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()),],
),
]

View File

@ -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",
),
),
]

View File

@ -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

2
pyproject.toml Normal file
View File

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

View File

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