diff --git a/.flake8 b/.flake8 deleted file mode 100644 index c5b7892..0000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length=120 -ignore = E126,E128,W504 -exclude = */migrations/[0-9]*, .pyscripts/*,pyenv*,.pyenv*, .git, .venv \ No newline at end of file diff --git a/.gitignore b/.gitignore index deb3dea..64e22ef 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +.ruff_cache/ # Translations *.mo @@ -89,4 +90,6 @@ ENV/ env.bak/ venv.bak/ -.vscode \ No newline at end of file +.vscode + +lockbox/media \ No newline at end of file diff --git a/Makefile b/Makefile index 44e4e15..947146c 100644 --- a/Makefile +++ b/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 \ No newline at end of file diff --git a/README.md b/README.md index e69de29..c4f77ea 100644 --- a/README.md +++ b/README.md @@ -0,0 +1 @@ +# Lockbox File Sharing Service \ No newline at end of file diff --git a/lockbox/common/admin.py b/lockbox/common/admin.py index 8c38f3f..0f4002d 100644 --- a/lockbox/common/admin.py +++ b/lockbox/common/admin.py @@ -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) diff --git a/lockbox/common/apps.py b/lockbox/common/apps.py index 01cca2f..e283781 100644 --- a/lockbox/common/apps.py +++ b/lockbox/common/apps.py @@ -1,4 +1,6 @@ -from django.apps import AppConfig +from django.apps import ( + AppConfig, +) class CommonConfig(AppConfig): diff --git a/lockbox/common/constants.py b/lockbox/common/constants.py new file mode 100644 index 0000000..3ec6ef2 --- /dev/null +++ b/lockbox/common/constants.py @@ -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, + }, +} diff --git a/lockbox/common/migrations/0001_initial.py b/lockbox/common/migrations/0001_initial.py new file mode 100644 index 0000000..50d4aae --- /dev/null +++ b/lockbox/common/migrations/0001_initial.py @@ -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, + }, + ), + ] diff --git a/lockbox/common/models.py b/lockbox/common/models.py index 3185b53..43606a9 100644 --- a/lockbox/common/models.py +++ b/lockbox/common/models.py @@ -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) diff --git a/lockbox/common/tests.py b/lockbox/common/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/lockbox/common/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/lockbox/common/tests/__init__.py b/lockbox/common/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lockbox/common/utils.py b/lockbox/common/utils.py new file mode 100644 index 0000000..dd775af --- /dev/null +++ b/lockbox/common/utils.py @@ -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 diff --git a/lockbox/common/views.py b/lockbox/common/views.py index 91ea44a..fd0e044 100644 --- a/lockbox/common/views.py +++ b/lockbox/common/views.py @@ -1,3 +1,3 @@ -from django.shortcuts import render +# from django.shortcuts import render # Create your views here. diff --git a/lockbox/lockbox/settings.py b/lockbox/lockbox/settings.py index 066a1cb..2a00491 100644 --- a/lockbox/lockbox/settings.py +++ b/lockbox/lockbox/settings.py @@ -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' \ No newline at end of file +AUTH_USER_MODEL = 'user.LockboxUser' diff --git a/lockbox/lockbox/urls.py b/lockbox/lockbox/urls.py index d8f5095..04d0042 100644 --- a/lockbox/lockbox/urls.py +++ b/lockbox/lockbox/urls.py @@ -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)) diff --git a/lockbox/storage/__init__.py b/lockbox/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lockbox/storage/admin.py b/lockbox/storage/admin.py new file mode 100644 index 0000000..04d250f --- /dev/null +++ b/lockbox/storage/admin.py @@ -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) diff --git a/lockbox/storage/apps.py b/lockbox/storage/apps.py new file mode 100644 index 0000000..433500d --- /dev/null +++ b/lockbox/storage/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class StorageConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'storage' diff --git a/lockbox/storage/migrations/0001_initial.py b/lockbox/storage/migrations/0001_initial.py new file mode 100644 index 0000000..e00cf19 --- /dev/null +++ b/lockbox/storage/migrations/0001_initial.py @@ -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', + }, + ), + ] diff --git a/lockbox/storage/migrations/__init__.py b/lockbox/storage/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lockbox/storage/models.py b/lockbox/storage/models.py new file mode 100644 index 0000000..1a6e70e --- /dev/null +++ b/lockbox/storage/models.py @@ -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") diff --git a/lockbox/storage/storage_backend.py b/lockbox/storage/storage_backend.py new file mode 100644 index 0000000..92e4dd1 --- /dev/null +++ b/lockbox/storage/storage_backend.py @@ -0,0 +1,2 @@ +from django.core.files.storage import FileSystemStorage + diff --git a/lockbox/storage/tests/__init__.py b/lockbox/storage/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lockbox/storage/views.py b/lockbox/storage/views.py new file mode 100644 index 0000000..fd0e044 --- /dev/null +++ b/lockbox/storage/views.py @@ -0,0 +1,3 @@ +# from django.shortcuts import render + +# Create your views here. diff --git a/lockbox/user/admin.py b/lockbox/user/admin.py index 8c38f3f..6f3349b 100644 --- a/lockbox/user/admin.py +++ b/lockbox/user/admin.py @@ -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) diff --git a/lockbox/user/managers.py b/lockbox/user/managers.py index 6176c14..8c157c8 100644 --- a/lockbox/user/managers.py +++ b/lockbox/user/managers.py @@ -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): diff --git a/lockbox/user/migrations/0001_initial.py b/lockbox/user/migrations/0001_initial.py index 6a786b7..fb3d32e 100644 --- a/lockbox/user/migrations/0001_initial.py +++ b/lockbox/user/migrations/0001_initial.py @@ -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')), ], diff --git a/lockbox/user/models.py b/lockbox/user/models.py index f546ca7..9216734 100644 --- a/lockbox/user/models.py +++ b/lockbox/user/models.py @@ -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. diff --git a/lockbox/user/tests.py b/lockbox/user/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/lockbox/user/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/lockbox/user/tests/test_user.py b/lockbox/user/tests/test_user.py index b4c346a..d51d2f1 100644 --- a/lockbox/user/tests/test_user.py +++ b/lockbox/user/tests/test_user.py @@ -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. - """ \ No newline at end of file + Test user related functions are working correctly. + """ + def test_stub(self): + assert True diff --git a/lockbox/user/views.py b/lockbox/user/views.py index 91ea44a..fd0e044 100644 --- a/lockbox/user/views.py +++ b/lockbox/user/views.py @@ -1,3 +1,3 @@ -from django.shortcuts import render +# from django.shortcuts import render # Create your views here. diff --git a/poetry.lock b/poetry.lock index 4f0326d..fa67480 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 7d69375..787098f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"]