Added very basic storage model and WIP API
This commit is contained in:
parent
3f89efcb36
commit
ead978715c
4
.flake8
4
.flake8
|
|
@ -1,4 +0,0 @@
|
|||
[flake8]
|
||||
max-line-length=120
|
||||
ignore = E126,E128,W504
|
||||
exclude = */migrations/[0-9]*, .pyscripts/*,pyenv*,.pyenv*, .git, .venv
|
||||
|
|
@ -47,6 +47,7 @@ coverage.xml
|
|||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
.ruff_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
|
@ -89,4 +90,6 @@ ENV/
|
|||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
.vscode
|
||||
.vscode
|
||||
|
||||
lockbox/media
|
||||
5
Makefile
5
Makefile
|
|
@ -1,11 +1,8 @@
|
|||
lint:
|
||||
@flake8 --config=.flake8 /dev/null $(shell git diff --name-only HEAD | grep '\.py$$' )
|
||||
@ruff check $(shell git diff --diff-filter=ACM --name-only HEAD | grep '\.py$$' )
|
||||
|
||||
stampreqs:
|
||||
poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
|
||||
test:
|
||||
pytest --cov=. --cov-report term-missing
|
||||
|
||||
testcap:
|
||||
pytest --cov=. --cov-report term-missing -s
|
||||
|
|
@ -1,3 +1,10 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from common.models import Configuration
|
||||
|
||||
|
||||
class LockboxModelAdmin(admin.ModelAdmin):
|
||||
readonly_fields = Configuration.readonly_fields
|
||||
|
||||
|
||||
admin.site.register(Configuration, LockboxModelAdmin)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
from django.apps import (
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
|
||||
class CommonConfig(AppConfig):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
class UPLOAD_STATUS_TYPES:
|
||||
UPLOADING = "uploading"
|
||||
COMPLETED = "completed"
|
||||
ABANDONED = "abandoned"
|
||||
|
||||
|
||||
# Config
|
||||
|
||||
CONFIG_KEYS = {
|
||||
"EXPIRATION_DELTA_MINUTES": {
|
||||
"description": "Date created + this delta at which file expires",
|
||||
"verbose_name": "File expiration delta (minutes)",
|
||||
"native_type": int,
|
||||
"default": 120,
|
||||
},
|
||||
|
||||
"ABANDONED_DELTA_MINUTES": {
|
||||
"description": "Date created + this delta at which a file is marked as abandoned",
|
||||
"verbose_name": "Uncompleted file abandoned max age",
|
||||
"native_type": int,
|
||||
"default": 20,
|
||||
},
|
||||
|
||||
"ABANDONED_EXPIRED_SCAN_INTERVAL": {
|
||||
"description": "Scan and scrub abandoned or expired uploads",
|
||||
"verbose_name": "Scan interval for abandoned/expired files",
|
||||
"native_type": int,
|
||||
"default": 20,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 4.2.10 on 2024-02-12 08:39
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Configuration',
|
||||
fields=[
|
||||
('lid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='lockbox ID')),
|
||||
('date_created', models.DateTimeField(blank=True, help_text='date at which this object was created', verbose_name='date created')),
|
||||
('date_updated', models.DateTimeField(blank=True, help_text='date at which this object was last updated', verbose_name='date updated')),
|
||||
('key', models.CharField(choices=[('EXPIRATION_DELTA_MINUTES', 'Date created + this delta at which file expires'), ('ABANDONED_DELTA_MINUTES', 'Date created + this delta at which a file is marked as abandoned'), ('ABANDONED_EXPIRED_SCAN_INTERVAL', 'Scan and scrub abandoned or expired uploads')], help_text='internal configuration key name', max_length=50)),
|
||||
('value', models.CharField(help_text='actual DB config value', max_length=1024)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,24 +1,41 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
from typing import Any, NamedTuple
|
||||
from uuid import uuid4
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.constants import CONFIG_KEYS
|
||||
from common.utils import cast_to_native_type
|
||||
|
||||
|
||||
class LockboxBase(models.Model): # pragma: no cover
|
||||
lid = models.UUIDField(
|
||||
primary_key=True,
|
||||
default=uuid4
|
||||
default=uuid4,
|
||||
verbose_name=_("lockbox ID"),
|
||||
)
|
||||
|
||||
date_created = models.DateTimeField(
|
||||
verbose_name=_("date created")
|
||||
verbose_name=_("date created"),
|
||||
help_text=_("date at which this object was created"),
|
||||
null=False,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
date_updated = models.DateTimeField(
|
||||
verbose_name=_("date updated"),
|
||||
null=True
|
||||
help_text=_("date at which this object was last updated"),
|
||||
null=False,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
readonly_fields = [
|
||||
"date_created",
|
||||
"date_updated",
|
||||
"lid",
|
||||
]
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
|
@ -29,6 +46,47 @@ class LockboxBase(models.Model): # pragma: no cover
|
|||
if not self.date_created:
|
||||
self.date_created = now
|
||||
|
||||
self.date_update = now
|
||||
self.date_updated = now
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.__class__.__name__} Object {self.lid}"
|
||||
|
||||
|
||||
class Config(NamedTuple):
|
||||
key: str
|
||||
value: Any
|
||||
native_type: type # change to type
|
||||
description: str
|
||||
source: str
|
||||
default: Any
|
||||
|
||||
|
||||
class Configuration(LockboxBase):
|
||||
|
||||
CONFIG_KEY_CHOICES = (
|
||||
(key, value["description"]) for key, value in CONFIG_KEYS.items()
|
||||
)
|
||||
|
||||
key = models.CharField(
|
||||
choices=CONFIG_KEY_CHOICES,
|
||||
max_length=50,
|
||||
null=False,
|
||||
blank=False,
|
||||
help_text=_("internal configuration key name"),
|
||||
)
|
||||
|
||||
value = models.CharField(
|
||||
max_length=1024,
|
||||
null=False,
|
||||
blank=False,
|
||||
help_text=_("actual DB config value"),
|
||||
)
|
||||
|
||||
readonly_fields = LockboxBase.readonly_fields
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
native_type = CONFIG_KEYS[self.key]["native_type"]
|
||||
cast_to_native_type(self.key, self.value, native_type)
|
||||
return super().save(*args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
from os import getenv
|
||||
from unicodedata import normalize
|
||||
|
||||
from common.constants import CONFIG_KEYS
|
||||
|
||||
|
||||
def normalize_string(string, form="NFKC"):
|
||||
return normalize(form, string)
|
||||
|
||||
def cast_to_native_type(key, value, native_type):
|
||||
try:
|
||||
return native_type(value)
|
||||
except ValueError as e:
|
||||
message = (
|
||||
f"Received unexpected value type for configuration key {key}\nValue: {value}\nExpected type : {native_type}"
|
||||
)
|
||||
raise ValueError(message) from e
|
||||
|
||||
|
||||
def get_config(key):
|
||||
from common.models import Config, Configuration
|
||||
config = Config(CONFIG_KEYS[key])
|
||||
|
||||
obj = Configuration.objects.filter(key=key).first()
|
||||
|
||||
if obj:
|
||||
config.value = cast_to_native_type(key, obj.value, config.native_type)
|
||||
config.source = "db"
|
||||
return config
|
||||
|
||||
value = getenv(key)
|
||||
|
||||
if value:
|
||||
config.value = cast_to_native_type(key, value, config.native_type)
|
||||
config.source = "env_variable"
|
||||
return config
|
||||
|
||||
config.value = config.default
|
||||
config.source = "default"
|
||||
return config
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
# from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
"""
|
||||
Lockbox File Sharing
|
||||
"""
|
||||
"""Lockbox File Sharing"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
SECRET_KEY = os.getenv("LOCKBOX_SECRET_KEY")
|
||||
DEBUG = os.getenv("LOCKBOX_DEBUG", False)
|
||||
DEBUG = os.getenv("LOCKBOX_DEBUG")
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
|
@ -23,12 +21,13 @@ INSTALLED_APPS = [
|
|||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
|
||||
# Vendors
|
||||
|
||||
|
||||
# Apps
|
||||
'common',
|
||||
'user',
|
||||
'storage',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
@ -67,7 +66,7 @@ DATABASES = {
|
|||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -101,6 +100,10 @@ USE_TZ = True
|
|||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
# Storage
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
MEDIA_URL = "files/"
|
||||
|
||||
# Default primary key field type
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
AUTH_USER_MODEL = 'user.LockboxUser'
|
||||
AUTH_USER_MODEL = 'user.LockboxUser'
|
||||
|
|
|
|||
|
|
@ -14,9 +14,13 @@ Including another URLconf
|
|||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
|
||||
urlpatterns.extend(static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from storage.models import File
|
||||
|
||||
|
||||
class FileAdmin(admin.ModelAdmin):
|
||||
readonly_fields = File.readonly_fields
|
||||
|
||||
admin.site.register(File, FileAdmin)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class StorageConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'storage'
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Generated by Django 4.2.10 on 2024-02-12 09:59
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import storage.models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='File',
|
||||
fields=[
|
||||
('lid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='lockbox ID')),
|
||||
('date_created', models.DateTimeField(blank=True, help_text='date at which this object was created', verbose_name='date created')),
|
||||
('date_updated', models.DateTimeField(blank=True, help_text='date at which this object was last updated', verbose_name='date updated')),
|
||||
('name', models.CharField(help_text='display name of this file', max_length=255, verbose_name='name')),
|
||||
('extension', models.CharField(blank=True, help_text='reported filesystem extension (not mime type)', max_length=128, null=True, verbose_name='extension')),
|
||||
('file', models.FileField(help_text='actual file', upload_to=storage.models.get_upload_path, verbose_name='file')),
|
||||
('position', models.PositiveBigIntegerField(default=0, help_text='current position of uploaded bytes', verbose_name='position')),
|
||||
('status', models.CharField(choices=[('uploading', 'uploading'), ('completed', 'completed'), ('abandoned', 'abandoned')], default='uploading', help_text='upload status for file', max_length=9, verbose_name='status')),
|
||||
('date_completed', models.DateTimeField(blank=True, help_text="datetime at which this file's upload was completed", null=True, verbose_name='completed on')),
|
||||
('expires', models.BooleanField(default=False, help_text="will be scrubbed on 'date_expires'", verbose_name='expires')),
|
||||
('delete_on_expiration', models.BooleanField(default=False, help_text='will be deleted if expired and expires is true', verbose_name='delete on expiration')),
|
||||
('size_on_disk', models.PositiveBigIntegerField(blank=True, help_text='total size on disk for this file', null=True, verbose_name='size on disk (bytes)')),
|
||||
('owner', models.ForeignKey(blank=True, help_text='owner of this file', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='files_owned', to=settings.AUTH_USER_MODEL, verbose_name='owner')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'file',
|
||||
'verbose_name_plural': 'files',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from common.constants import CONFIG_KEYS, UPLOAD_STATUS_TYPES
|
||||
from common.models import LockboxBase
|
||||
from common.utils import get_config, normalize_string
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def get_upload_path(instance, filename):
|
||||
filename = normalize_string(filename) + ".part"
|
||||
file_subdir = settings.MEDIA_ROOT / str(instance.lid)
|
||||
|
||||
if not Path.exists(file_subdir):
|
||||
Path.mkdir(file_subdir)
|
||||
|
||||
return Path(str(instance.lid)) / Path(filename)
|
||||
|
||||
|
||||
class File(LockboxBase):
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name = _("name"),
|
||||
help_text=_("display name of this file"),
|
||||
)
|
||||
|
||||
extension = models.CharField(
|
||||
max_length=128,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("extension"),
|
||||
help_text=_("reported filesystem extension (not mime type)"),
|
||||
)
|
||||
|
||||
file = models.FileField(
|
||||
upload_to=get_upload_path,
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name=_("file"),
|
||||
help_text=_("actual file"),
|
||||
)
|
||||
|
||||
position = models.PositiveBigIntegerField(
|
||||
default=0,
|
||||
blank=False,
|
||||
null=False,
|
||||
verbose_name=_("position"),
|
||||
help_text=_("current position of uploaded bytes"),
|
||||
)
|
||||
|
||||
UPLOAD_CHOICES = (
|
||||
(UPLOAD_STATUS_TYPES.UPLOADING, _(UPLOAD_STATUS_TYPES.UPLOADING)),
|
||||
(UPLOAD_STATUS_TYPES.COMPLETED, _(UPLOAD_STATUS_TYPES.COMPLETED)),
|
||||
(UPLOAD_STATUS_TYPES.ABANDONED, _(UPLOAD_STATUS_TYPES.ABANDONED)),
|
||||
)
|
||||
|
||||
status = models.CharField(
|
||||
max_length=9,
|
||||
choices=UPLOAD_CHOICES,
|
||||
default=UPLOAD_STATUS_TYPES.UPLOADING,
|
||||
blank=False,
|
||||
null=False,
|
||||
verbose_name=_("status"),
|
||||
help_text=_("upload status for file"),
|
||||
)
|
||||
|
||||
date_completed = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("completed on"),
|
||||
help_text=_("datetime at which this file's upload was completed"),
|
||||
)
|
||||
|
||||
owner = models.ForeignKey(
|
||||
"user.LockboxUser",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="files_owned",
|
||||
verbose_name=_("owner"),
|
||||
help_text=_("owner of this file"),
|
||||
)
|
||||
|
||||
expires = models.BooleanField(
|
||||
null=False,
|
||||
blank=False,
|
||||
default=False,
|
||||
verbose_name = _("expires"),
|
||||
help_text=_("will be scrubbed on 'date_expires'"),
|
||||
)
|
||||
|
||||
delete_on_expiration = models.BooleanField(
|
||||
null=False,
|
||||
blank=False,
|
||||
default=False,
|
||||
verbose_name=_("delete on expiration"),
|
||||
help_text=_("will be deleted if expired and expires is true"),
|
||||
)
|
||||
|
||||
size_on_disk = models.PositiveBigIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("size on disk (bytes)"),
|
||||
help_text=_("total size on disk for this file"),
|
||||
)
|
||||
|
||||
readonly_fields = [
|
||||
"extension",
|
||||
"position",
|
||||
"status",
|
||||
"date_completed",
|
||||
"size_on_disk",
|
||||
*LockboxBase.readonly_fields,
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("file")
|
||||
verbose_name_plural = _("files")
|
||||
|
||||
@property
|
||||
def md5(self):
|
||||
if getattr(self, "_md5", None) is None:
|
||||
md5 = hashlib.md5() # noqa:S324 this is needed due to how chunked files work.
|
||||
for chunk in self.file.chunks():
|
||||
md5.update(chunk)
|
||||
self._md5 = md5.hexdigest()
|
||||
return self._md5
|
||||
|
||||
@property
|
||||
def date_expires(self):
|
||||
return self.date_created + get_config(CONFIG_KEYS["EXPIRATION_DELTA_MINUTES"])
|
||||
|
||||
@property
|
||||
def abandoned(self):
|
||||
return self.date_created + get_config(CONFIG_KEYS["ABANDONED_DELTA_MINUTES"])
|
||||
|
||||
@property
|
||||
def expired(self):
|
||||
return self.date_expires <= timezone.now()
|
||||
|
||||
def delete(self, *args, delete_file=True, **kwargs):
|
||||
if self.file:
|
||||
storage, path = self.file.storage, self.file.path
|
||||
super().delete(*args, **kwargs)
|
||||
if self.file and delete_file:
|
||||
storage.delete(path)
|
||||
|
||||
def get_file_handler_bytes(self):
|
||||
self.file.close()
|
||||
self.file.open(mode="rb")
|
||||
return UploadedFile(file=self.file, name=self.filename, size=self.offset)
|
||||
|
||||
def append_chunk(self, chunk, chunk_size=None, save=None):
|
||||
# file handler might be open for some bizzare reason
|
||||
self.file.close()
|
||||
|
||||
with Path.open(self.file.path, mode="ab") as fh:
|
||||
fh.write(
|
||||
chunk.read(),
|
||||
)
|
||||
|
||||
if chunk_size is not None:
|
||||
# file is chunked
|
||||
self.postition += chunk_size
|
||||
elif hasattr(chunk, "size"):
|
||||
self.postition += chunk.size
|
||||
else:
|
||||
# file is one shot (small file)
|
||||
self.postition = self.file.size
|
||||
self._md5 = None
|
||||
if save:
|
||||
self.save()
|
||||
self.file.close()
|
||||
|
||||
|
||||
# class FileShare(LockboxBase):
|
||||
# file = models.ForeignKey(
|
||||
# "storage.File",
|
||||
# null=False,
|
||||
# blank=False,
|
||||
# on_delete=models.CASCADE,
|
||||
# related_name="shares",
|
||||
# )
|
||||
|
||||
# def __str__(self):
|
||||
# return self.file.name
|
||||
|
||||
# class Meta:
|
||||
# verbose_name = _("share")
|
||||
# verbose_name_plural = _("shares")
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from django.core.files.storage import FileSystemStorage
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from user.models import LockboxUser
|
||||
|
||||
|
||||
class LockboxUserAdmin(admin.ModelAdmin):
|
||||
readonly_fields = LockboxUser.readonly_fields
|
||||
|
||||
admin.site.register(LockboxUser, LockboxUserAdmin)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from django.contrib.auth.base_user import BaseUserManager
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db import models
|
||||
|
||||
|
||||
class LockboxUserManager(BaseUserManager):
|
||||
def create_user(self, username, password, **extra_fields):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 4.2.10 on 2024-02-10 10:51
|
||||
# Generated by Django 4.2.10 on 2024-02-12 08:39
|
||||
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
|
|
@ -25,10 +25,10 @@ class Migration(migrations.Migration):
|
|||
('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')),
|
||||
('lid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('date_created', models.DateTimeField(verbose_name='date created')),
|
||||
('date_updated', models.DateTimeField(null=True, verbose_name='date updated')),
|
||||
('alias', models.SlugField(blank=True, help_text='An alias or nickname to remember who this is', max_length=32, null=True, unique=True, verbose_name='name')),
|
||||
('lid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='lockbox ID')),
|
||||
('date_created', models.DateTimeField(blank=True, help_text='date at which this object was created', verbose_name='date created')),
|
||||
('date_updated', models.DateTimeField(blank=True, help_text='date at which this object was last updated', verbose_name='date updated')),
|
||||
('alias', models.SlugField(blank=True, help_text='an alias or nickname to remember who this is', max_length=32, null=True, unique=True, verbose_name='name')),
|
||||
('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')),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
from common.models import LockboxBase
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from common.models import LockboxBase
|
||||
from user.managers import LockboxUserManager
|
||||
|
||||
|
||||
|
|
@ -15,8 +13,7 @@ class LockboxUser(AbstractUser, LockboxBase):
|
|||
unique=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[],
|
||||
help_text=_("An alias or nickname to remember who this is")
|
||||
help_text=_("an alias or nickname to remember who this is"),
|
||||
)
|
||||
|
||||
# Void this stuff.
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -1,13 +1,10 @@
|
|||
import pytest
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from user.models import LockboxUser
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db()
|
||||
class TestUser:
|
||||
"""
|
||||
Test util default creation functions are working.
|
||||
"""
|
||||
Test user related functions are working correctly.
|
||||
"""
|
||||
def test_stub(self):
|
||||
assert True
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
# from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
|
|
|||
|
|
@ -202,6 +202,32 @@ pytest = ">=7.0.0"
|
|||
docs = ["sphinx", "sphinx-rtd-theme"]
|
||||
testing = ["Django", "django-configurations (>=2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.2.1"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"},
|
||||
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"},
|
||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"},
|
||||
{file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"},
|
||||
{file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"},
|
||||
{file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"},
|
||||
{file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"},
|
||||
{file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"},
|
||||
{file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"},
|
||||
{file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"},
|
||||
{file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.4.4"
|
||||
|
|
@ -232,4 +258,4 @@ files = [
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "3.12"
|
||||
content-hash = "824df996658026864e02f217b5df60d87723b3f06f3e8c5c1d265abf7851b978"
|
||||
content-hash = "1a0f3ef0953e311eaf5daaff9b7980e0211fb0e5195344a28881b731bca3b7eb"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ django = "~4.2.0"
|
|||
pytest = "^8.0.0"
|
||||
pytest-django = "^4.8.0"
|
||||
pytest-cov = "^4.1.0"
|
||||
ruff = "^0.2.1"
|
||||
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
|
@ -46,6 +47,34 @@ omit = [
|
|||
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
"*/migrations/[0-9]*",
|
||||
".pyscripts/*",
|
||||
"pyenv*",
|
||||
".pyenv*",
|
||||
".git",
|
||||
".venv",
|
||||
]
|
||||
|
||||
line-length = 120
|
||||
target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["ALL"]
|
||||
ignore = [
|
||||
"ANN",
|
||||
"D",
|
||||
"DJ001",
|
||||
"DJ012",
|
||||
"ERA001",
|
||||
"N801",
|
||||
"Q000",
|
||||
"RUF012",
|
||||
"TRY",
|
||||
"S101",
|
||||
"SLF001",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue