add django backend in /core
ported from laravel in c3lf/lfbackend repo
This commit is contained in:
parent
dd75c2b0d6
commit
1f41b81b8f
29 changed files with 1201 additions and 0 deletions
129
core/.gitignore
vendored
Normal file
129
core/.gitignore
vendored
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
0
core/core/__init__.py
Normal file
0
core/core/__init__.py
Normal file
16
core/core/asgi.py
Normal file
16
core/core/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for core project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
183
core/core/settings.py
Normal file
183
core/core/settings.py
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
"""
|
||||||
|
Django settings for core project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 4.2.7.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import dotenv
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
dotenv.load_dotenv(BASE_DIR / '.env')
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'django-insecure-tm*$w_14iqbiy-!7(8#ba7j+_@(7@rf2&a^!=shs&$03b%2*rv'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
SYSTEM3_VERSION = "0.0.0-dev.0"
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'django_extensions',
|
||||||
|
'rest_framework',
|
||||||
|
'rest_framework.authtoken',
|
||||||
|
'drf_yasg',
|
||||||
|
'channels',
|
||||||
|
'files',
|
||||||
|
'inventory',
|
||||||
|
]
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'TEST_REQUEST_DEFAULT_FORMAT': 'json'
|
||||||
|
}
|
||||||
|
|
||||||
|
SWAGGER_SETTINGS = {
|
||||||
|
'SECURITY_DEFINITIONS': {
|
||||||
|
'api_key': {
|
||||||
|
'type': 'apiKey',
|
||||||
|
'in': 'header',
|
||||||
|
'name': 'Authorization'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'USE_SESSION_AUTH': False,
|
||||||
|
'JSON_EDITOR': True,
|
||||||
|
'DEFAULT_INFO': 'core.urls.openapi_info',
|
||||||
|
}
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'core.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [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 = 'core.wsgi.application'
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/4.2/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',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_ROOT = 'staticfiles'
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
MEDIA_ROOT = 'userfiles'
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
|
STORAGES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django.core.files.storage.FileSystemStorage',
|
||||||
|
'OPTIONS': {
|
||||||
|
'base_url': MEDIA_URL,
|
||||||
|
'location': BASE_DIR / MEDIA_ROOT
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'staticfiles': {
|
||||||
|
'BACKEND': 'django.core.files.storage.FileSystemStorage',
|
||||||
|
'OPTIONS': {
|
||||||
|
'base_url': STATIC_URL,
|
||||||
|
'location': BASE_DIR / STATIC_ROOT
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024 * 128 # 128 MB
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'channels.layers.InMemoryChannelLayer',
|
||||||
|
# 'BACKEND': 'asgi_redis.RedisChannelLayer',
|
||||||
|
# 'CONFIG': {
|
||||||
|
# 'hosts': [('localhost', 6379)],
|
||||||
|
# },
|
||||||
|
'ROUTING': 'example.routing.channel_routing',
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_RUNNER = 'core.test_runner.FastTestRunner'
|
21
core/core/test_runner.py
Normal file
21
core/core/test_runner.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.test.runner import DiscoverRunner
|
||||||
|
|
||||||
|
|
||||||
|
class FastTestRunner(DiscoverRunner):
|
||||||
|
def setup_test_environment(self):
|
||||||
|
super(FastTestRunner, self).setup_test_environment()
|
||||||
|
# Don't write files
|
||||||
|
settings.STORAGES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django.core.files.storage.InMemoryStorage',
|
||||||
|
'OPTIONS': {
|
||||||
|
'base_url': '/media/',
|
||||||
|
'location': '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
# Bonus: Use a faster password hasher. This REALLY helps.
|
||||||
|
settings.PASSWORD_HASHERS = (
|
||||||
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
|
)
|
27
core/core/urls.py
Normal file
27
core/core/urls.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
"""
|
||||||
|
URL configuration for core project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/4.2/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
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.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
from .version import get_info
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('1/', include('inventory.api_v1')),
|
||||||
|
path('1/', include('files.api_v1')),
|
||||||
|
path('', get_info),
|
||||||
|
]
|
13
core/core/version.py
Normal file
13
core/core/version.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from rest_framework.decorators import api_view
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from .settings import SYSTEM3_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
def get_info(request):
|
||||||
|
return Response({
|
||||||
|
"framework_version": SYSTEM3_VERSION,
|
||||||
|
"api_min_version": "1.0",
|
||||||
|
"api_max_version": "1.0",
|
||||||
|
})
|
16
core/core/wsgi.py
Normal file
16
core/core/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for core project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
0
core/files/__init__.py
Normal file
0
core/files/__init__.py
Normal file
10
core/files/admin.py
Normal file
10
core/files/admin.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from files.models import File
|
||||||
|
|
||||||
|
|
||||||
|
class FileAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(File, FileAdmin)
|
25
core/files/api_v1.py
Normal file
25
core/files/api_v1.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from rest_framework import serializers, viewsets, routers
|
||||||
|
|
||||||
|
from files.models import File
|
||||||
|
|
||||||
|
|
||||||
|
class FileSerializer(serializers.ModelSerializer):
|
||||||
|
data = serializers.CharField(max_length=1000000, write_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = File
|
||||||
|
fields = ['hash', 'data']
|
||||||
|
read_only_fields = ['hash']
|
||||||
|
|
||||||
|
|
||||||
|
class FileViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = FileSerializer
|
||||||
|
queryset = File.objects.all()
|
||||||
|
lookup_field = 'hash'
|
||||||
|
|
||||||
|
|
||||||
|
router = routers.SimpleRouter()
|
||||||
|
router.register(r'files', FileViewSet, basename='files')
|
||||||
|
router.register(r'file', FileViewSet, basename='files')
|
||||||
|
|
||||||
|
urlpatterns = router.urls
|
57
core/files/media_urls.py
Normal file
57
core/files/media_urls.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
from coverage.annotate import os
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.urls import path
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.decorators import api_view
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from core.settings import MEDIA_ROOT
|
||||||
|
from files.models import File
|
||||||
|
|
||||||
|
|
||||||
|
@swagger_auto_schema(method='GET', auto_schema=None)
|
||||||
|
@api_view(['GET'])
|
||||||
|
def media_urls(request, hash_path):
|
||||||
|
try:
|
||||||
|
file = File.objects.get(file=hash_path)
|
||||||
|
|
||||||
|
return HttpResponse(status=status.HTTP_200_OK,
|
||||||
|
content_type=file.mime_type,
|
||||||
|
headers={
|
||||||
|
'X-Accel-Redirect': f'/redirect_media/{hash_path}',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
}) # TODO Expires and Cache-Control
|
||||||
|
|
||||||
|
except File.DoesNotExist:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
@swagger_auto_schema(method='GET', auto_schema=None)
|
||||||
|
@api_view(['GET'])
|
||||||
|
def thumbnail_urls(request, size, hash_path):
|
||||||
|
if size not in [32, 64, 256]:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
|
try:
|
||||||
|
file = File.objects.get(file=hash_path)
|
||||||
|
if not os.path.exists(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}'):
|
||||||
|
from PIL import Image
|
||||||
|
iamge = Image.open(file.file)
|
||||||
|
iamge.thumbnail((size, size))
|
||||||
|
iamge.save(MEDIA_ROOT + f'/media/thumbnails/{size}/{hash_path}', quality=90)
|
||||||
|
|
||||||
|
return HttpResponse(status=status.HTTP_200_OK,
|
||||||
|
content_type=file.mime_type,
|
||||||
|
headers={
|
||||||
|
'X-Accel-Redirect': f'/redirect_thumbnail/{size}/{hash_path}',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
}) # TODO Expires and Cache-Control
|
||||||
|
|
||||||
|
except File.DoesNotExist:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('<int:size>/<path:hash_path>', thumbnail_urls),
|
||||||
|
path('<path:hash_path>', media_urls),
|
||||||
|
]
|
30
core/files/migrations/0001_initial.py
Normal file
30
core/files/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 4.2.7 on 2023-11-18 11:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import files.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('inventory', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='File',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('updated_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('file', models.ImageField(upload_to=files.models.hash_upload)),
|
||||||
|
('mime_type', models.CharField(max_length=255)),
|
||||||
|
('hash', models.CharField(max_length=64, unique=True)),
|
||||||
|
('item', models.ForeignKey(blank=True, db_column='iid', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='files', to='inventory.item')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
core/files/migrations/__init__.py
Normal file
0
core/files/migrations/__init__.py
Normal file
57
core/files/models.py
Normal file
57
core/files/models.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.db import models, IntegrityError
|
||||||
|
from django_softdelete.models import SoftDeleteModel
|
||||||
|
|
||||||
|
from inventory.models import Item
|
||||||
|
|
||||||
|
|
||||||
|
def hash_upload(instance, filename):
|
||||||
|
return f"{instance.hash[:2]}/{instance.hash[2:4]}/{instance.hash[4:6]}/{instance.hash[6:]}"
|
||||||
|
|
||||||
|
|
||||||
|
class FileManager(models.Manager):
|
||||||
|
def get_or_create(self, **kwargs):
|
||||||
|
if 'data' in kwargs and type(kwargs['data']) == str:
|
||||||
|
import base64
|
||||||
|
from hashlib import sha256
|
||||||
|
content = base64.b64decode(kwargs['data'], validate=True)
|
||||||
|
kwargs.pop('data')
|
||||||
|
content_hash = sha256(content).hexdigest()
|
||||||
|
kwargs['file'] = ContentFile(content, content_hash)
|
||||||
|
kwargs['hash'] = content_hash
|
||||||
|
else:
|
||||||
|
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
|
||||||
|
try:
|
||||||
|
return self.get(hash=kwargs['hash']), False
|
||||||
|
except self.model.DoesNotExist:
|
||||||
|
return self.create(**kwargs), True
|
||||||
|
|
||||||
|
def create(self, **kwargs):
|
||||||
|
if 'data' in kwargs and type(kwargs['data']) == str:
|
||||||
|
import base64
|
||||||
|
from hashlib import sha256
|
||||||
|
content = base64.b64decode(kwargs['data'], validate=True)
|
||||||
|
kwargs.pop('data')
|
||||||
|
content_hash = sha256(content).hexdigest()
|
||||||
|
kwargs['file'] = ContentFile(content, content_hash)
|
||||||
|
kwargs['hash'] = content_hash
|
||||||
|
elif 'file' in kwargs and 'hash' in kwargs and type(kwargs['file']) == ContentFile:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
|
||||||
|
if not self.filter(hash=kwargs['hash']).exists():
|
||||||
|
return super().create(**kwargs)
|
||||||
|
else:
|
||||||
|
raise IntegrityError('File with this hash already exists')
|
||||||
|
|
||||||
|
|
||||||
|
class File(models.Model):
|
||||||
|
item = models.ForeignKey(Item, models.CASCADE, db_column='iid', null=True, blank=True, related_name='files')
|
||||||
|
created_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
updated_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
deleted_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
file = models.ImageField(upload_to=hash_upload)
|
||||||
|
mime_type = models.CharField(max_length=255, null=False, blank=False)
|
||||||
|
hash = models.CharField(max_length=64, null=False, blank=False, unique=True)
|
||||||
|
|
||||||
|
objects = FileManager()
|
47
core/files/tests.py
Normal file
47
core/files/tests.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
from django.test import TestCase, Client
|
||||||
|
|
||||||
|
from files.models import File
|
||||||
|
from inventory.models import Event, Container, Item
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
|
||||||
|
|
||||||
|
class FileTestCase(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.event = Event.objects.create(slug='EVENT', name='Event')
|
||||||
|
self.box = Container.objects.create(name='BOX')
|
||||||
|
|
||||||
|
def test_list_files(self):
|
||||||
|
import base64
|
||||||
|
item = File.objects.create(data=base64.b64encode(b"foo").decode('utf-8'))
|
||||||
|
response = client.get('/1/files/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json()[0]['hash'], item.hash)
|
||||||
|
self.assertEqual(len(response.json()[0]['hash']), 64)
|
||||||
|
|
||||||
|
def test_one_file(self):
|
||||||
|
import base64
|
||||||
|
item = File.objects.create(data=base64.b64encode(b"foo").decode('utf-8'))
|
||||||
|
response = client.get(f'/1/file/{item.hash}/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json()['hash'], item.hash)
|
||||||
|
self.assertEqual(len(response.json()['hash']), 64)
|
||||||
|
|
||||||
|
def test_create_file(self):
|
||||||
|
import base64
|
||||||
|
Item.objects.create(container=self.box, event=self.event, description='1')
|
||||||
|
item = Item.objects.create(container=self.box, event=self.event, description='2')
|
||||||
|
response = client.post('/1/file/', {'data': base64.b64encode(b"foo").decode('utf-8')}, content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(len(response.json()['hash']), 64)
|
||||||
|
|
||||||
|
def test_delete_file(self):
|
||||||
|
import base64
|
||||||
|
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
||||||
|
File.objects.create(item=item, data=base64.b64encode(b"foo").decode('utf-8'))
|
||||||
|
file = File.objects.create(item=item, data=base64.b64encode(b"bar").decode('utf-8'))
|
||||||
|
self.assertEqual(len(File.objects.all()), 2)
|
||||||
|
response = client.delete(f'/1/file/{file.hash}/')
|
||||||
|
self.assertEqual(response.status_code, 204)
|
0
core/inventory/__init__.py
Normal file
0
core/inventory/__init__.py
Normal file
24
core/inventory/admin.py
Normal file
24
core/inventory/admin.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from inventory.models import Item, Container, Event
|
||||||
|
|
||||||
|
|
||||||
|
class ItemAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Item, ItemAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Container, ContainerAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class EventAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Event, EventAdmin)
|
143
core/inventory/api_v1.py
Normal file
143
core/inventory/api_v1.py
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
from rest_framework import routers, viewsets, serializers
|
||||||
|
from rest_framework.decorators import api_view
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from files.models import File
|
||||||
|
from inventory.models import Event, Container, Item
|
||||||
|
|
||||||
|
|
||||||
|
class EventSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Event
|
||||||
|
fields = ['eid', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end']
|
||||||
|
read_only_fields = ['eid']
|
||||||
|
|
||||||
|
|
||||||
|
class EventViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = EventSerializer
|
||||||
|
queryset = Event.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerSerializer(serializers.ModelSerializer):
|
||||||
|
itemCount = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Container
|
||||||
|
fields = ['cid', 'name', 'itemCount']
|
||||||
|
read_only_fields = ['cid', 'itemCount']
|
||||||
|
|
||||||
|
def get_itemCount(self, instance):
|
||||||
|
return Item.objects.filter(container=instance.cid).count()
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = ContainerSerializer
|
||||||
|
queryset = Container.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ItemSerializer(serializers.ModelSerializer):
|
||||||
|
cid = serializers.SerializerMethodField()
|
||||||
|
box = serializers.SerializerMethodField()
|
||||||
|
file = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Item
|
||||||
|
fields = ['cid', 'box', 'uid', 'description', 'file']
|
||||||
|
read_only_fields = ['uid']
|
||||||
|
|
||||||
|
def get_cid(self, instance):
|
||||||
|
return instance.container.cid
|
||||||
|
|
||||||
|
def get_box(self, instance):
|
||||||
|
return instance.container.name
|
||||||
|
|
||||||
|
def get_file(self, instance):
|
||||||
|
if len(instance.files.all()) > 0:
|
||||||
|
return instance.files.all().order_by('-created_at')[0].hash
|
||||||
|
return None
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if 'cid' in data:
|
||||||
|
container = Container.objects.get(cid=data['cid'])
|
||||||
|
internal = super().to_internal_value(data)
|
||||||
|
internal['container'] = container
|
||||||
|
return internal
|
||||||
|
return super().to_internal_value(data)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
attrs.pop('dataImage', None)
|
||||||
|
return super().validate(attrs)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
if 'dataImage' in validated_data:
|
||||||
|
file = File.objects.create(data=validated_data['dataImage'], iid=validated_data['iid'])
|
||||||
|
validated_data.pop('dataImage')
|
||||||
|
return Item.objects.create(**validated_data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
if 'returned' in validated_data:
|
||||||
|
if validated_data['returned']:
|
||||||
|
validated_data['returned_at'] = datetime.now()
|
||||||
|
validated_data.pop('returned')
|
||||||
|
if 'dataImage' in validated_data:
|
||||||
|
file = File.objects.create(data=validated_data['dataImage'], iid=instance.iid)
|
||||||
|
validated_data.pop('dataImage')
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
def search_items(request, event_slug, query):
|
||||||
|
event = Event.objects.get(slug=event_slug)
|
||||||
|
query_tokens = query.split(' ')
|
||||||
|
q = Item.objects.filter(event=event)
|
||||||
|
for token in query_tokens:
|
||||||
|
if token:
|
||||||
|
q = q.filter(description__icontains=token)
|
||||||
|
return Response(ItemSerializer(q, many=True).data)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET', 'POST'])
|
||||||
|
def item(request, event_slug):
|
||||||
|
event = Event.objects.get(slug=event_slug)
|
||||||
|
if request.method == 'GET':
|
||||||
|
return Response(ItemSerializer(Item.objects.filter(event=event), many=True).data)
|
||||||
|
elif request.method == 'POST':
|
||||||
|
validated_data = ItemSerializer(data=request.data)
|
||||||
|
if validated_data.is_valid():
|
||||||
|
validated_data.save(event=event)
|
||||||
|
return Response(validated_data.data, status=201)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET', 'PUT', 'DELETE'])
|
||||||
|
def item_by_id(request, event_slug, id):
|
||||||
|
try:
|
||||||
|
event = Event.objects.get(slug=event_slug)
|
||||||
|
item = Item.objects.get(event=event, uid=id)
|
||||||
|
if request.method == 'GET':
|
||||||
|
return Response(ItemSerializer(item).data)
|
||||||
|
elif request.method == 'PUT':
|
||||||
|
validated_data = ItemSerializer(item, data=request.data)
|
||||||
|
if validated_data.is_valid():
|
||||||
|
validated_data.save()
|
||||||
|
return Response(validated_data.data)
|
||||||
|
elif request.method == 'DELETE':
|
||||||
|
item.delete()
|
||||||
|
return Response(status=204)
|
||||||
|
except Item.DoesNotExist:
|
||||||
|
return Response(status=404)
|
||||||
|
|
||||||
|
|
||||||
|
router = routers.SimpleRouter()
|
||||||
|
router.register(r'events', EventViewSet, basename='events')
|
||||||
|
router.register(r'boxes', ContainerViewSet, basename='boxes')
|
||||||
|
router.register(r'box', ContainerViewSet, basename='boxes')
|
||||||
|
|
||||||
|
urlpatterns = router.urls + [
|
||||||
|
path('<event_slug>/items/', item),
|
||||||
|
path('<event_slug>/items/<query>/', search_items),
|
||||||
|
path('<event_slug>/item/', item),
|
||||||
|
path('<event_slug>/item/<id>/', item_by_id),
|
||||||
|
]
|
54
core/inventory/migrations/0001_initial.py
Normal file
54
core/inventory/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# Generated by Django 4.2.7 on 2023-11-18 11:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Container',
|
||||||
|
fields=[
|
||||||
|
('cid', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('created_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('updated_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Event',
|
||||||
|
fields=[
|
||||||
|
('eid', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('slug', models.CharField(max_length=255, unique=True)),
|
||||||
|
('start', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('end', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('pre_start', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('post_end', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('created_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('updated_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Item',
|
||||||
|
fields=[
|
||||||
|
('iid', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('uid', models.IntegerField()),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('returned_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('created_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('updated_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('container', models.ForeignKey(db_column='cid', on_delete=django.db.models.deletion.CASCADE, to='inventory.container')),
|
||||||
|
('event', models.ForeignKey(db_column='eid', on_delete=django.db.models.deletion.CASCADE, to='inventory.event')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('uid', 'event')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
core/inventory/migrations/__init__.py
Normal file
0
core/inventory/migrations/__init__.py
Normal file
48
core/inventory/models.py
Normal file
48
core/inventory/models.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.db import models, IntegrityError
|
||||||
|
from django_softdelete.models import SoftDeleteModel
|
||||||
|
|
||||||
|
|
||||||
|
class ItemManager(models.Manager):
|
||||||
|
|
||||||
|
def create(self, **kwargs):
|
||||||
|
if 'uid' in kwargs:
|
||||||
|
raise ValueError('uid must not be set manually')
|
||||||
|
uid = Item.objects.filter(event=kwargs['event']).count() + 1
|
||||||
|
kwargs['uid'] = uid
|
||||||
|
return super().create(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Item(models.Model):
|
||||||
|
iid = models.AutoField(primary_key=True)
|
||||||
|
uid = models.IntegerField()
|
||||||
|
description = models.TextField()
|
||||||
|
event = models.ForeignKey('Event', models.CASCADE, db_column='eid')
|
||||||
|
container = models.ForeignKey('Container', models.CASCADE, db_column='cid')
|
||||||
|
returned_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
updated_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
objects = ItemManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (('uid', 'event'),)
|
||||||
|
|
||||||
|
|
||||||
|
class Container(models.Model):
|
||||||
|
cid = models.AutoField(primary_key=True)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
created_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
updated_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Event(models.Model):
|
||||||
|
eid = models.AutoField(primary_key=True)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
slug = models.CharField(max_length=255, unique=True)
|
||||||
|
start = models.DateTimeField(blank=True, null=True)
|
||||||
|
end = models.DateTimeField(blank=True, null=True)
|
||||||
|
pre_start = models.DateTimeField(blank=True, null=True)
|
||||||
|
post_end = models.DateTimeField(blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
updated_at = models.DateTimeField(blank=True, null=True)
|
0
core/inventory/tests/__init__.py
Normal file
0
core/inventory/tests/__init__.py
Normal file
34
core/inventory/tests/test_api.py
Normal file
34
core/inventory/tests/test_api.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from django.test import TestCase, Client
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
|
||||||
|
|
||||||
|
class ApiTest(TestCase):
|
||||||
|
|
||||||
|
def test_root(self):
|
||||||
|
from core.settings import SYSTEM3_VERSION
|
||||||
|
response = client.get('/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json()["framework_version"], SYSTEM3_VERSION)
|
||||||
|
|
||||||
|
def test_events(self):
|
||||||
|
response = client.get('/1/events/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), [])
|
||||||
|
|
||||||
|
def test_containers(self):
|
||||||
|
response = client.get('/1/boxes/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), [])
|
||||||
|
|
||||||
|
def test_files(self):
|
||||||
|
response = client.get('/1/files/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), [])
|
||||||
|
|
||||||
|
def test_items(self):
|
||||||
|
from inventory.models import Event
|
||||||
|
Event.objects.create(slug='TEST1', name='Event')
|
||||||
|
response = client.get('/1/TEST1/items/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), [])
|
59
core/inventory/tests/test_containers.py
Normal file
59
core/inventory/tests/test_containers.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
from django.test import TestCase, Client
|
||||||
|
from inventory.models import Container
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerTestCase(TestCase):
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
response = client.get('/1/boxes/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), [])
|
||||||
|
|
||||||
|
def test_members(self):
|
||||||
|
Container.objects.create(name='BOX')
|
||||||
|
response = client.get('/1/boxes/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(len(response.json()), 1)
|
||||||
|
self.assertEqual(response.json()[0]['cid'], 1)
|
||||||
|
self.assertEqual(response.json()[0]['name'], 'BOX')
|
||||||
|
self.assertEqual(response.json()[0]['itemCount'], 0)
|
||||||
|
|
||||||
|
def test_multi_members(self):
|
||||||
|
Container.objects.create(name='BOX 1')
|
||||||
|
Container.objects.create(name='BOX 2')
|
||||||
|
Container.objects.create(name='BOX 3')
|
||||||
|
response = client.get('/1/boxes/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(len(response.json()), 3)
|
||||||
|
|
||||||
|
def test_create_container(self):
|
||||||
|
response = client.post('/1/box/', {'name': 'BOX'})
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(response.json()['cid'], 1)
|
||||||
|
self.assertEqual(response.json()['name'], 'BOX')
|
||||||
|
self.assertEqual(response.json()['itemCount'], 0)
|
||||||
|
self.assertEqual(len(Container.objects.all()), 1)
|
||||||
|
self.assertEqual(Container.objects.all()[0].cid, 1)
|
||||||
|
self.assertEqual(Container.objects.all()[0].name, 'BOX')
|
||||||
|
|
||||||
|
def test_update_container(self):
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
box = Container.objects.create(name='BOX 1')
|
||||||
|
response = APIClient().put(f'/1/box/{box.cid}/', {'name': 'BOX 2'})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json()['cid'], 1)
|
||||||
|
self.assertEqual(response.json()['name'], 'BOX 2')
|
||||||
|
self.assertEqual(response.json()['itemCount'], 0)
|
||||||
|
self.assertEqual(len(Container.objects.all()), 1)
|
||||||
|
self.assertEqual(Container.objects.all()[0].cid, 1)
|
||||||
|
self.assertEqual(Container.objects.all()[0].name, 'BOX 2')
|
||||||
|
|
||||||
|
def test_delete_container(self):
|
||||||
|
box = Container.objects.create(name='BOX 1')
|
||||||
|
Container.objects.create(name='BOX 2')
|
||||||
|
self.assertEqual(len(Container.objects.all()), 2)
|
||||||
|
response = client.delete(f'/1/box/{box.cid}/')
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertEqual(len(Container.objects.all()), 1)
|
56
core/inventory/tests/test_events.py
Normal file
56
core/inventory/tests/test_events.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
from django.test import TestCase, Client
|
||||||
|
from inventory.models import Event
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
|
||||||
|
|
||||||
|
class EventTestCase(TestCase):
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
response = client.get('/1/events/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), [])
|
||||||
|
|
||||||
|
def test_members(self):
|
||||||
|
Event.objects.create(slug='EVENT', name='Event')
|
||||||
|
response = client.get('/1/events/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(len(response.json()), 1)
|
||||||
|
self.assertEqual(response.json()[0]['slug'], 'EVENT')
|
||||||
|
self.assertEqual(response.json()[0]['name'], 'Event')
|
||||||
|
|
||||||
|
def test_multi_members(self):
|
||||||
|
Event.objects.create(slug='EVENT1', name='Event 1')
|
||||||
|
Event.objects.create(slug='EVENT2', name='Event 2')
|
||||||
|
Event.objects.create(slug='EVENT3', name='Event 3')
|
||||||
|
response = client.get('/1/events/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(len(response.json()), 3)
|
||||||
|
|
||||||
|
def test_create_event(self):
|
||||||
|
response = client.post('/1/events/', {'slug': 'EVENT', 'name': 'Event'})
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(response.json()['slug'], 'EVENT')
|
||||||
|
self.assertEqual(response.json()['name'], 'Event')
|
||||||
|
self.assertEqual(len(Event.objects.all()), 1)
|
||||||
|
self.assertEqual(Event.objects.all()[0].slug, 'EVENT')
|
||||||
|
self.assertEqual(Event.objects.all()[0].name, 'Event')
|
||||||
|
|
||||||
|
def test_update_event(self):
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
event = Event.objects.create(slug='EVENT1', name='Event 1')
|
||||||
|
response = APIClient().put(f'/1/events/{event.eid}/', {'slug': 'EVENT2', 'name': 'Event 2 new'})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json()['slug'], 'EVENT2')
|
||||||
|
self.assertEqual(response.json()['name'], 'Event 2 new')
|
||||||
|
self.assertEqual(len(Event.objects.all()), 1)
|
||||||
|
self.assertEqual(Event.objects.all()[0].slug, 'EVENT2')
|
||||||
|
self.assertEqual(Event.objects.all()[0].name, 'Event 2 new')
|
||||||
|
|
||||||
|
def test_remove_event(self):
|
||||||
|
event = Event.objects.create(slug='EVENT1', name='Event 1')
|
||||||
|
Event.objects.create(slug='EVENT2', name='Event 2')
|
||||||
|
self.assertEqual(len(Event.objects.all()), 2)
|
||||||
|
response = client.delete(f'/1/events/{event.eid}/')
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertEqual(len(Event.objects.all()), 1)
|
93
core/inventory/tests/test_items.py
Normal file
93
core/inventory/tests/test_items.py
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
from django.test import TestCase, Client
|
||||||
|
|
||||||
|
from files.models import File
|
||||||
|
from inventory.models import Event, Container, Item
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
|
||||||
|
|
||||||
|
class ItemTestCase(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.event = Event.objects.create(slug='EVENT', name='Event')
|
||||||
|
self.box = Container.objects.create(name='BOX')
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
response = client.get(f'/1/{self.event.slug}/item/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.content, b'[]')
|
||||||
|
|
||||||
|
def test_members(self):
|
||||||
|
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
||||||
|
response = client.get(f'/1/{self.event.slug}/item/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), [{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None}])
|
||||||
|
|
||||||
|
def test_members_with_file(self):
|
||||||
|
import base64
|
||||||
|
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
||||||
|
file = File.objects.create(item=item, data=base64.b64encode(b"foo").decode('utf-8'))
|
||||||
|
response = client.get(f'/1/{self.event.slug}/item/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(),
|
||||||
|
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': file.hash}])
|
||||||
|
|
||||||
|
def test_multi_members(self):
|
||||||
|
Item.objects.create(container=self.box, event=self.event, description='1')
|
||||||
|
Item.objects.create(container=self.box, event=self.event, description='2')
|
||||||
|
Item.objects.create(container=self.box, event=self.event, description='3')
|
||||||
|
response = client.get(f'/1/{self.event.slug}/item/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(len(response.json()), 3)
|
||||||
|
|
||||||
|
def test_create_item(self):
|
||||||
|
response = client.post(f'/1/{self.event.slug}/item/', {'cid': self.box.cid, 'description': '1'})
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(response.json(), {'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None})
|
||||||
|
self.assertEqual(len(Item.objects.all()), 1)
|
||||||
|
self.assertEqual(Item.objects.all()[0].uid, 1)
|
||||||
|
self.assertEqual(Item.objects.all()[0].description, '1')
|
||||||
|
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
|
||||||
|
|
||||||
|
#def test_create_item_fail(self):
|
||||||
|
# response = client.post(f'/1/{self.event.slug}/item/', {'cid': self.box.cid})
|
||||||
|
# self.assertEqual(response.status_code, 500)
|
||||||
|
|
||||||
|
def test_update_item(self):
|
||||||
|
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
||||||
|
response = client.put(f'/1/{self.event.slug}/item/{item.uid}/', {'description': '2'}, content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(),
|
||||||
|
{'uid': 1, 'description': '2', 'box': 'BOX', 'cid': self.box.cid, 'file': None})
|
||||||
|
self.assertEqual(len(Item.objects.all()), 1)
|
||||||
|
self.assertEqual(Item.objects.all()[0].uid, 1)
|
||||||
|
self.assertEqual(Item.objects.all()[0].description, '2')
|
||||||
|
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
|
||||||
|
|
||||||
|
def test_delete_item(self):
|
||||||
|
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
||||||
|
Item.objects.create(container=self.box, event=self.event, description='2')
|
||||||
|
self.assertEqual(len(Item.objects.all()), 2)
|
||||||
|
response = client.delete(f'/1/{self.event.slug}/item/{item.uid}/')
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertEqual(len(Item.objects.all()), 1)
|
||||||
|
|
||||||
|
def test_delete_item2(self):
|
||||||
|
Item.objects.create(container=self.box, event=self.event, description='1')
|
||||||
|
item2 = Item.objects.create(container=self.box, event=self.event, description='2')
|
||||||
|
self.assertEqual(len(Item.objects.all()), 2)
|
||||||
|
response = client.delete(f'/1/{self.event.slug}/item/{item2.uid}/')
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertEqual(len(Item.objects.all()), 1)
|
||||||
|
item3 = Item.objects.create(container=self.box, event=self.event, description='3')
|
||||||
|
self.assertEqual(item3.uid, 2)
|
||||||
|
self.assertEqual(len(Item.objects.all()), 2)
|
||||||
|
|
||||||
|
def test_item_count(self):
|
||||||
|
Item.objects.create(container=self.box, event=self.event, description='1')
|
||||||
|
Item.objects.create(container=self.box, event=self.event, description='2')
|
||||||
|
response = client.get('/1/boxes/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(len(response.json()), 1)
|
||||||
|
self.assertEqual(response.json()[0]['itemCount'], 2)
|
22
core/manage.py
Executable file
22
core/manage.py
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
37
core/requirements.txt
Normal file
37
core/requirements.txt
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
aiosmtpd==1.4.4.post2
|
||||||
|
asgi-redis==1.4.3
|
||||||
|
asgiref==3.7.2
|
||||||
|
async-timeout==4.0.3
|
||||||
|
atpublic==4.0
|
||||||
|
attrs==23.1.0
|
||||||
|
certifi==2023.7.22
|
||||||
|
channels==4.0.0
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
coreapi==2.3.3
|
||||||
|
coreschema==0.0.4
|
||||||
|
coverage==7.3.2
|
||||||
|
Django==4.2.7
|
||||||
|
django-extensions==3.2.3
|
||||||
|
django-soft-delete==0.9.21
|
||||||
|
djangorestframework==3.14.0
|
||||||
|
drf-yasg==1.21.7
|
||||||
|
idna==3.4
|
||||||
|
inflection==0.5.1
|
||||||
|
itypes==1.2.0
|
||||||
|
Jinja2==3.1.2
|
||||||
|
MarkupSafe==2.1.3
|
||||||
|
msgpack-python==0.5.6
|
||||||
|
openapi-codec==1.3.2
|
||||||
|
packaging==23.2
|
||||||
|
Pillow==10.1.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
pytz==2023.3.post1
|
||||||
|
PyYAML==6.0.1
|
||||||
|
redis==5.0.1
|
||||||
|
requests==2.31.0
|
||||||
|
six==1.16.0
|
||||||
|
sqlparse==0.4.4
|
||||||
|
typing_extensions==4.8.0
|
||||||
|
uritemplate==4.1.1
|
||||||
|
urllib3==2.1.0
|
||||||
|
websockets==12.0
|
Loading…
Reference in a new issue