Compare commits
No commits in common. "7369db8512cad491614f541987119486e9513614" and "a69bf30440f5f6c1756bdf3aea5e6e2fb597740c" have entirely different histories.
7369db8512
...
a69bf30440
66 changed files with 20719 additions and 17469 deletions
0
web/.gitignore → .gitignore
vendored
0
web/.gitignore → .gitignore
vendored
129
core/.gitignore
vendored
129
core/.gitignore
vendored
|
@ -1,129 +0,0 @@
|
||||||
# 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/
|
|
|
@ -1,16 +0,0 @@
|
||||||
"""
|
|
||||||
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()
|
|
|
@ -1,196 +0,0 @@
|
||||||
"""
|
|
||||||
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 sys
|
|
||||||
|
|
||||||
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 = [os.getenv('HTTP_HOST', 'localhost')]
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if 'test' in sys.argv:
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': ':memory:',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
|
||||||
'HOST': os.getenv('DB_HOST', 'localhost'),
|
|
||||||
'PORT': os.getenv('DB_PORT', '3306'),
|
|
||||||
'NAME': os.getenv('DB_NAME', 'system3'),
|
|
||||||
'USER': os.getenv('DB_USER', 'system3'),
|
|
||||||
'PASSWORD': os.getenv('DB_PASSWORD', 'system3'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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 = os.getenv('STATIC_ROOT', 'staticfiles')
|
|
||||||
STATIC_URL = '/static/'
|
|
||||||
|
|
||||||
MEDIA_ROOT = os.getenv('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'
|
|
|
@ -1,21 +0,0 @@
|
||||||
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',
|
|
||||||
)
|
|
|
@ -1,27 +0,0 @@
|
||||||
"""
|
|
||||||
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('api/1/', include('inventory.api_v1')),
|
|
||||||
path('api/1/', include('files.api_v1')),
|
|
||||||
path('api/', get_info),
|
|
||||||
]
|
|
|
@ -1,13 +0,0 @@
|
||||||
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",
|
|
||||||
})
|
|
|
@ -1,16 +0,0 @@
|
||||||
"""
|
|
||||||
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()
|
|
|
@ -1,10 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from files.models import File
|
|
||||||
|
|
||||||
|
|
||||||
class FileAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(File, FileAdmin)
|
|
|
@ -1,25 +0,0 @@
|
||||||
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
|
|
|
@ -1,57 +0,0 @@
|
||||||
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),
|
|
||||||
]
|
|
|
@ -1,30 +0,0 @@
|
||||||
# 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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,57 +0,0 @@
|
||||||
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()
|
|
|
@ -1,47 +0,0 @@
|
||||||
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('/api/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'/api/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('/api/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'/api/1/file/{file.hash}/')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
|
@ -1,24 +0,0 @@
|
||||||
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)
|
|
|
@ -1,143 +0,0 @@
|
||||||
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),
|
|
||||||
]
|
|
|
@ -1,54 +0,0 @@
|
||||||
# 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')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,48 +0,0 @@
|
||||||
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)
|
|
|
@ -1,34 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
|
|
||||||
client = Client()
|
|
||||||
|
|
||||||
|
|
||||||
class ApiTest(TestCase):
|
|
||||||
|
|
||||||
def test_root(self):
|
|
||||||
from core.settings import SYSTEM3_VERSION
|
|
||||||
response = client.get('/api/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()["framework_version"], SYSTEM3_VERSION)
|
|
||||||
|
|
||||||
def test_events(self):
|
|
||||||
response = client.get('/api/1/events/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_containers(self):
|
|
||||||
response = client.get('/api/1/boxes/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_files(self):
|
|
||||||
response = client.get('/api/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('/api/1/TEST1/items/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
|
@ -1,59 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
from inventory.models import Container
|
|
||||||
|
|
||||||
client = Client()
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerTestCase(TestCase):
|
|
||||||
|
|
||||||
def test_empty(self):
|
|
||||||
response = client.get('/api/1/boxes/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_members(self):
|
|
||||||
Container.objects.create(name='BOX')
|
|
||||||
response = client.get('/api/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('/api/1/boxes/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 3)
|
|
||||||
|
|
||||||
def test_create_container(self):
|
|
||||||
response = client.post('/api/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'/api/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'/api/1/box/{box.cid}/')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
||||||
self.assertEqual(len(Container.objects.all()), 1)
|
|
|
@ -1,56 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
from inventory.models import Event
|
|
||||||
|
|
||||||
client = Client()
|
|
||||||
|
|
||||||
|
|
||||||
class EventTestCase(TestCase):
|
|
||||||
|
|
||||||
def test_empty(self):
|
|
||||||
response = client.get('/api/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('/api/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('/api/1/events/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 3)
|
|
||||||
|
|
||||||
def test_create_event(self):
|
|
||||||
response = client.post('/api/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'/api/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'/api/1/events/{event.eid}/')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
||||||
self.assertEqual(len(Event.objects.all()), 1)
|
|
|
@ -1,93 +0,0 @@
|
||||||
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'/api/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'/api/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'/api/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'/api/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'/api/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'/api/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'/api/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'/api/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'/api/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('/api/1/boxes/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 1)
|
|
||||||
self.assertEqual(response.json()[0]['itemCount'], 2)
|
|
|
@ -1,22 +0,0 @@
|
||||||
#!/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()
|
|
|
@ -1,35 +0,0 @@
|
||||||
aiosmtpd==1.4.4.post2
|
|
||||||
asgi-redis
|
|
||||||
async-timeout==4.0.3
|
|
||||||
atpublic==4.0
|
|
||||||
attrs==23.1.0
|
|
||||||
channels
|
|
||||||
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
|
|
||||||
itypes==1.2.0
|
|
||||||
Jinja2==3.1.2
|
|
||||||
MarkupSafe==2.1.3
|
|
||||||
msgpack-python==0.5.6
|
|
||||||
mysqlclient
|
|
||||||
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
|
|
||||||
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
|
|
11914
package-lock.json
generated
Normal file
11914
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -11,7 +11,7 @@
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
||||||
"@fortawesome/vue-fontawesome": "^0.1.8",
|
"@fortawesome/vue-fontawesome": "^0.1.8",
|
||||||
"axios": "^1.6.2",
|
"axios": "^0.19.0",
|
||||||
"base-64": "^0.1.0",
|
"base-64": "^0.1.0",
|
||||||
"bootstrap": "^4.3.1",
|
"bootstrap": "^4.3.1",
|
||||||
"core-js": "^3.3.2",
|
"core-js": "^3.3.2",
|
||||||
|
@ -19,10 +19,10 @@
|
||||||
"jquery": "^3.4.1",
|
"jquery": "^3.4.1",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"luxon": "^1.21.3",
|
"luxon": "^1.21.3",
|
||||||
|
"node-sass": "^4.13.0",
|
||||||
"popper.js": "^1.16.0",
|
"popper.js": "^1.16.0",
|
||||||
"ramda": "^0.26.1",
|
"ramda": "^0.26.1",
|
||||||
"sass": "^1.19.0",
|
"sass-loader": "^8.0.0",
|
||||||
"sass-loader": "^10.4.1",
|
|
||||||
"utf8": "^3.0.0",
|
"utf8": "^3.0.0",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-debounce": "^2.2.0",
|
"vue-debounce": "^2.2.0",
|
||||||
|
@ -31,8 +31,12 @@
|
||||||
"vuex-router-sync": "^5.0.0"
|
"vuex-router-sync": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^5.0.8",
|
"@vue/cli-plugin-babel": "^4.0.0",
|
||||||
"@vue/cli-service": "^5.0.8",
|
"@vue/cli-plugin-eslint": "^4.0.0",
|
||||||
|
"@vue/cli-service": "^4.0.0",
|
||||||
|
"babel-eslint": "^10.0.3",
|
||||||
|
"eslint": "^5.16.0",
|
||||||
|
"eslint-plugin-vue": "^5.0.0",
|
||||||
"vue-template-compiler": "^2.6.10"
|
"vue-template-compiler": "^2.6.10"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
@ -38,7 +38,6 @@
|
||||||
aria-label="Search"
|
aria-label="Search"
|
||||||
v-debounce:500ms="myFunc"
|
v-debounce:500ms="myFunc"
|
||||||
@input="searchEventItems($event.target.value)"
|
@input="searchEventItems($event.target.value)"
|
||||||
disabled
|
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
<p>Herzlich Willkommen bei Lost&Found von $Veranstaltung!</p>
|
<p>Herzlich Willkommen bei Lost&Found von $Veranstaltung!</p>
|
||||||
<p>Deine Aufgaben sind es verloren gegangene Gegenstände anzunehmen und zu registrieren, sowie
|
<p>Deine Aufgaben sind es verloren gegangene Gegenstände anzunehmen und zu registrieren, sowie
|
||||||
Gegenstände ihren Besitzenden zurückzubringen.</p>
|
Gegenstände ihren Besitzenden zurückzubringen.</p>
|
||||||
<p><b>Bitte den Inhalt des Lost+Founds nicht offen liegen lassen oder rumzeigen. Erst beschreiben
|
<p>Bitte den Inhalt des Lost+Founds nicht offen liegen lassen und rumzeigen. Erst beschreiben
|
||||||
lassen, dann zeigen.</b></p>
|
lassen, dann zeigen.</p>
|
||||||
<h3>Found (Jemand bringt einen verloren gegangen Gegenstand vorbei)</h3>
|
<h3>Found (Jemand bringt einen verloren gegangen Gegenstand vorbei)</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Möglichst viele Informationen über die Umstände heraus
|
<li>Möglichst viele Informationen über die Umstände heraus
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
gut möglich dass der Himmel und andere Villages eigene, kleinere Lost+Founds während des Events
|
gut möglich dass der Himmel und andere Villages eigene, kleinere Lost+Founds während des Events
|
||||||
aufgemacht haben. Rumfragen lohnt sich also.
|
aufgemacht haben. Rumfragen lohnt sich also.
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>Achtung: Tickets werden erst nach Ende des Events bearbeitet.</b></li>
|
<li>Achtung: Tickets werden erst nach Ende des Events bearbeitet.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -62,7 +62,8 @@
|
||||||
<h2>Lost&Found (English version)</h2>
|
<h2>Lost&Found (English version)</h2>
|
||||||
<p>Welcome to Lost&Found of $Event!</p>
|
<p>Welcome to Lost&Found of $Event!</p>
|
||||||
<p>Your tasks are to accept and register lost items, as well as to return items to their owners.</p>
|
<p>Your tasks are to accept and register lost items, as well as to return items to their owners.</p>
|
||||||
<p><b>Please do not leave the contents of the Lost+Found lying around or show them around. Ask for a description of the item first, then show them (if the description seems correct).</b></p>
|
<p>Please do not leave the contents of the Lost+Found lying around and showing them around. First describe
|
||||||
|
them, then show them.</p>
|
||||||
<h3>Found (Someone brings a lost item)</h3>
|
<h3>Found (Someone brings a lost item)</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Get as much information as possible about the circumstances
|
<li>Get as much information as possible about the circumstances
|
||||||
|
@ -101,7 +102,7 @@
|
||||||
the Himmel and other Villages have their own, smaller Lost+Founds during the event. So it's worth asking
|
the Himmel and other Villages have their own, smaller Lost+Founds during the event. So it's worth asking
|
||||||
around.
|
around.
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>Attention: Tickets will not be processed until after the event.</b></li>
|
<li>Attention: Tickets will not be processed until after the event.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
16246
web/package-lock.json
generated
16246
web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue