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