25 Commits

Author SHA1 Message Date
b8ff82fbda Start porting to docker 2023-09-04 22:36:59 +02:00
217606e51d Merge dev and production settings into single settings file 2023-09-04 21:56:22 +02:00
566dafa87a Update to latest LTS django version 2023-09-04 21:33:34 +02:00
c567e38326 Add poetry config for Django ~4.2.5 2023-09-04 21:26:16 +02:00
210d67a898 Remove empty line 2023-09-04 20:46:22 +02:00
8636c513b7 Implement Paginated search using a mixin 2023-06-09 23:40:30 +02:00
3d263ca27c Make search stay active during page changes 2023-06-09 23:24:30 +02:00
1c56dd44f9 make component types hierarchial 2022-11-11 21:37:54 +01:00
a300d66f66 Merge branch 'master' into advanced-formset-search 2022-08-05 21:50:24 +02:00
1e20cb458f Add migration command for old file name structure 2022-08-05 21:47:55 +02:00
ac0f363a1e Indent file with TABS 2022-08-05 21:47:33 +02:00
b00cc19e61 Rework file upload structure to use subfolders for better file performance 2022-08-05 20:59:51 +02:00
52749da6e6 Start work on component types frontend 2022-07-30 15:11:23 +02:00
5fa6700bb4 Revert "Move debug settings to Postgresql database which is needed for fulltext search"
This reverts commit 0aadf4305f.
2022-07-30 14:07:35 +02:00
8c5d017ed1 Revert "Try out postgres fulltext search."
This reverts commit c47350f449.
2022-07-30 14:07:13 +02:00
c47350f449 Try out postgres fulltext search. 2022-04-20 17:33:24 +02:00
0aadf4305f Move debug settings to Postgresql database which is needed for fulltext search 2022-04-15 20:22:19 +02:00
b26c54dfce Add key parameters to stock 2022-02-21 19:45:42 +01:00
009ff5ae96 Improve a few queries 2022-01-11 20:45:01 +01:00
a566e198b8 Implement key parameter rendering for components. In Stock viewer still missing 2022-01-11 19:35:09 +01:00
ea623212bb Add package parameters 2022-01-10 19:22:34 +01:00
2bc0f3124c Add comparison method for parameter search 2022-01-07 16:24:22 +01:00
a34557499a Restructure Advanced search to a get request 2022-01-07 13:31:50 +01:00
35255cf4e9 Merge branch 'master' into advanced-formset-search 2022-01-04 20:16:07 +01:00
2d83c9ceec Hack ugly formset search. Must rewrite this 2022-01-04 00:14:06 +01:00
37 changed files with 972 additions and 621 deletions

View File

@@ -1 +0,0 @@
start_server.sh

View File

@@ -1,6 +1,17 @@
FROM alpine:latest FROM ubuntu:22.04
RUN apk add --no-cache python3 py3-pip python3-dev py3-setuptools gcc python3-dev jpeg-dev zlib-dev musl-dev py3-gunicorn
COPY . /home/shimatta/kenkyusho RUN apt-get update && apt-get install -y libpq-dev python3 python3-poetry nginx
WORKDIR /home/shimatta/kenkyusho COPY shimatta_kenkyusho /app/shimatta_kenkyusho
RUN python3 -m venv /home/shimatta/kenkyusho/.venv && . /home/shimatta/kenkyusho/.venv/bin/activate && pip install -r requirements.txt COPY poetry.lock /app/
ENTRYPOINT ["/home/shimatta/kenkyusho/entrypoint.sh"] COPY pyproject.toml /app/
RUN systemctl enable nginx
ARG UNAME=django
ARG UID=1000
ARG GID=1000
RUN groupadd -g $GID -o $UNAME
RUN useradd -m -u $UID -g $GID -o -s /bin/bash $UNAME
USER $UNAME
WORKDIR "/app"
RUN poetry install

View File

@@ -1,6 +0,0 @@
#!/bin/sh
source /home/shimatta/kenkyusho/.venv/bin/activate
cd /home/shimatta/kenkyusho/shimatta_kenkyusho
python manage.py migrate --settings shimatta_kenkyusho.settings_production
python manage.py collectstatic --settings shimatta_kenkyusho.settings_production --noinput
gunicorn -w 4 shimatta_kenkyusho.wsgi:application

226
poetry.lock generated Normal file
View File

@@ -0,0 +1,226 @@
[[package]]
name = "annotated-types"
version = "0.5.0"
description = "Reusable constraint types to use with typing.Annotated"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "asgiref"
version = "3.7.2"
description = "ASGI specs, helper code, and adapters"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""}
[package.extras]
tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
[[package]]
name = "crispy-bootstrap5"
version = "0.7"
description = "Bootstrap5 template pack for django-crispy-forms"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
django = ">=3.2"
django-crispy-forms = ">=1.13.0"
[package.extras]
test = ["pytest", "pytest-django"]
[[package]]
name = "django"
version = "4.2.5"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
category = "main"
optional = false
python-versions = ">=3.8"
[package.dependencies]
asgiref = ">=3.6.0,<4"
sqlparse = ">=0.3.1"
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras]
argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
[[package]]
name = "django-crispy-forms"
version = "2.0"
description = "Best way to have Django DRY forms"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
django = ">=3.2"
[[package]]
name = "django-filter"
version = "23.2"
description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
Django = ">=3.2"
[[package]]
name = "django-qr-code"
version = "3.1.1"
description = "An application that provides tools for displaying QR codes on your Django site."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
django = ">=3.2"
pydantic = "*"
pytz = "*"
segno = ">=1.5"
[[package]]
name = "djangorestframework"
version = "3.14.0"
description = "Web APIs for Django, made easy."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
django = ">=3.0"
pytz = "*"
[[package]]
name = "pillow"
version = "9.5.0"
description = "Python Imaging Library (Fork)"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
[[package]]
name = "psycopg2"
version = "2.9.7"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "pydantic"
version = "2.3.0"
description = "Data validation using Python type hints"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
annotated-types = ">=0.4.0"
pydantic-core = "2.6.3"
typing-extensions = ">=4.6.1"
[package.extras]
email = ["email-validator (>=2.0.0)"]
[[package]]
name = "pydantic-core"
version = "2.6.3"
description = ""
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pytz"
version = "2023.3"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "segno"
version = "1.5.2"
description = "QR Code and Micro QR Code generator for Python 2 and Python 3"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "sqlparse"
version = "0.4.4"
description = "A non-validating SQL parser."
category = "main"
optional = false
python-versions = ">=3.5"
[package.extras]
dev = ["flake8", "build"]
doc = ["sphinx"]
test = ["pytest", "pytest-cov"]
[[package]]
name = "typing-extensions"
version = "4.7.1"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "tzdata"
version = "2023.3"
description = "Provider of IANA time zone data"
category = "main"
optional = false
python-versions = ">=2"
[[package]]
name = "uwsgi"
version = "2.0.22"
description = "The uWSGI server"
category = "main"
optional = false
python-versions = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "6e065bbdff5da7659973fa67491d6093c83b8e57d4e8dde6968ee846d382bf65"
[metadata.files]
annotated-types = []
asgiref = []
crispy-bootstrap5 = []
django = []
django-crispy-forms = []
django-filter = []
django-qr-code = []
djangorestframework = []
pillow = []
psycopg2 = []
pydantic = []
pydantic-core = []
pytz = []
segno = []
sqlparse = []
typing-extensions = []
tzdata = []
uwsgi = []

23
pyproject.toml Normal file
View File

@@ -0,0 +1,23 @@
[tool.poetry]
name = "shimatta-kenkyusho"
version = "0.1.0"
description = "Shimatta Lab Inventory System"
authors = ["Mario Hüttel <mario.huettel@gmx.net>"]
license = "GPLv2"
[tool.poetry.dependencies]
python = "^3.10"
Django = "~4.2.5"
django-qr-code = "^3.1.1"
djangorestframework = "^3.14.0"
django-filter = "^23.2"
psycopg2 = "^2.9.7"
Pillow = "^9.3.0"
django-crispy-forms = "^2.0"
crispy-bootstrap5 = "^0.7"
uwsgi = "^2.0.22"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -1,26 +0,0 @@
asgiref==3.4.1
astroid==2.6.5
crispy-bootstrap5==0.6
Django==3.2.5
django-crispy-forms==1.13.0
django-filter==2.4.0
django-qr-code==2.2.0
django-rest-framework==0.1.0
django-tex==1.1.9.post1
djangorestframework==3.12.4
isort==5.9.3
Jinja2==3.0.1
lazy-object-proxy==1.6.0
MarkupSafe==2.0.1
mccabe==0.6.1
Pillow==8.3.1
pylint==2.9.6
pytz==2021.1
qrcode==7.2
segno==1.3.3
six==1.16.0
sqlparse==0.4.1
toml==0.10.2
wrapt==1.12.1
psycopg2-binary==2.9.9
gunicorn==21.2.0

View File

@@ -1,12 +1,11 @@
from django.contrib.auth.models import Group from django.contrib.auth.models import User, Group
from django.contrib.auth import get_user_model
from rest_framework import serializers from rest_framework import serializers
from parts import models as parts_models from parts import models as parts_models
class UserSerializer(serializers.HyperlinkedModelSerializer): class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = get_user_model() model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'groups'] fields = ['username', 'email', 'first_name', 'last_name', 'groups']
class GroupSerializer(serializers.HyperlinkedModelSerializer): class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:

View File

@@ -1,13 +1,11 @@
from django.urls import include, path from django.urls import include, path, re_path
from rest_framework import routers from rest_framework import routers
from .views import * from .views import *
from django.conf.urls import url
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'users', UserViewSet) router.register(r'users', UserViewSet)
router.register(r'groups', GroupViewSet) router.register(r'groups', GroupViewSet)
router.register(r'parts/storages', PartsStorageViewSet) router.register(r'parts/storages', PartsStorageViewSet)
router.register(r'parts/storage_templates', PartsStorageTemplatesViewSet, basename='storage-template')
router.register(r'parts/components', PartsComponentViewSet) router.register(r'parts/components', PartsComponentViewSet)
router.register(r'parts/stocks', PartsStockViewSet) router.register(r'parts/stocks', PartsStockViewSet)
router.register(r'parts/packages', PartsPackageViewSet) router.register(r'parts/packages', PartsPackageViewSet)
@@ -18,6 +16,6 @@ router.register(r'parts/component-param-types', PartsComponentParameterTypeViewS
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
url(r'^token-auth/', ObtainExpiringAuthToken.as_view()), re_path(r'^token-auth/', ObtainExpiringAuthToken.as_view()),
url(r'^token-logout/', TokenLogout.as_view()), re_path(r'^token-logout/', TokenLogout.as_view()),
] ]

View File

@@ -1,6 +1,5 @@
from django.shortcuts import render from django.shortcuts import render
from django.contrib.auth.models import Group from django.contrib.auth.models import User, Group
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from rest_framework import viewsets, status from rest_framework import viewsets, status
from rest_framework import permissions from rest_framework import permissions
@@ -26,7 +25,7 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
""" """
API endpoint that allows users to be viewed or edited. API endpoint that allows users to be viewed or edited.
""" """
queryset = get_user_model().objects.all() queryset = User.objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
filter_backends = [filters.SearchFilter] filter_backends = [filters.SearchFilter]
@@ -45,17 +44,8 @@ class PartsStorageViewSet(viewsets.ModelViewSet):
serializer_class = StorageSerializer serializer_class = StorageSerializer
permission_classes = [permissions.DjangoModelPermissions] permission_classes = [permissions.DjangoModelPermissions]
filter_backends = [django_filters.rest_framework.DjangoFilterBackend] filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
search_fields = ['id', 'name', 'parent_storage']
filterset_fields = ['id', 'name', 'parent_storage'] filterset_fields = ['id', 'name', 'parent_storage']
class PartsStorageTemplatesViewSet(viewsets.ReadOnlyModelViewSet):
queryset = parts_models.Storage.objects.filter(is_template=True)
serializer_class = StorageSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [filters.SearchFilter]
search_fields = ['id', 'name']
filterset_fields = ['id', 'name']
class PartsComponentViewSet(viewsets.ModelViewSet): class PartsComponentViewSet(viewsets.ModelViewSet):
queryset = parts_models.Component.objects.all() queryset = parts_models.Component.objects.all()
serializer_class = ComponentSerializer serializer_class = ComponentSerializer

View File

@@ -9,6 +9,7 @@ admin.site.register(parts_models.Manufacturer)
admin.site.register(parts_models.Storage) admin.site.register(parts_models.Storage)
admin.site.register(parts_models.Stock) admin.site.register(parts_models.Stock)
admin.site.register(parts_models.ComponentParameter) admin.site.register(parts_models.ComponentParameter)
admin.site.register(parts_models.PackageParameter)
admin.site.register(parts_models.ComponentParameterType) admin.site.register(parts_models.ComponentParameterType)
admin.site.register(parts_models.ComponentType) admin.site.register(parts_models.ComponentType)
admin.site.register(parts_models.Distributor) admin.site.register(parts_models.Distributor)

View File

@@ -1,26 +1,23 @@
from django import forms from django import forms
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.forms import widgets from django.forms import widgets
from django.core.exceptions import ValidationError
from parts import models as parts_models from parts import models as parts_models
from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter
import uuid import uuid
from django.urls import reverse from django.urls import reverse
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Row, Column from crispy_forms.layout import Layout, Fieldset, Row, Column
class AutoCompleteWidget(widgets.Input): class AutoCompleteWidget(widgets.Input):
template_name = 'widgets/autocomplete-foreign-key.html' template_name = 'widgets/autocomplete-foreign-key.html'
def __init__(self, api_search_url, image_field_name, foreign_model, name_field_name, prepend=None, *args, **kwargs): def __init__(self, api_search_url, image_field_name, foreign_model, name_field_name, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.image_field_name = image_field_name self.image_field_name = image_field_name
self.foreign_model = foreign_model self.foreign_model = foreign_model
self.api_search_url = api_search_url self.api_search_url = api_search_url
self.name_field_name = name_field_name self.name_field_name = name_field_name
self.prepend = prepend
def get_context(self, name, value, attrs): def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs) context = super().get_context(name, value, attrs)
@@ -50,25 +47,14 @@ class AutoCompleteWidget(widgets.Input):
'current_instance': instance, 'current_instance': instance,
'image': image, 'image': image,
'name_field_name': self.name_field_name, 'name_field_name': self.name_field_name,
'prepend': self.prepend,
'name': display_name, 'name': display_name,
} }
return context return context
class AutocompleteForeingKeyField(forms.UUIDField): class AutocompleteForeingKeyField(forms.UUIDField):
def __init__(self, def __init__(self, foreign_model=None, api_search_url=None, image_field_name='image', name_field_name='name', **kwargs):
foreign_model=None,
api_search_url=None,
image_field_name='image',
name_field_name='name',
prepend=None,
**kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.widget = AutoCompleteWidget(api_search_url, self.widget = AutoCompleteWidget(api_search_url, image_field_name, foreign_model, name_field_name)
image_field_name,
foreign_model,
name_field_name,
prepend)
self.foreign_model = foreign_model self.foreign_model = foreign_model
@@ -88,24 +74,13 @@ class AutocompleteForeingKeyField(forms.UUIDField):
raise ValidationError('Given element does not exist') raise ValidationError('Given element does not exist')
return obj return obj
class MyTestForm(forms.Form): class MyTestForm(forms.Form):
pass pass
class ChangeStorageForm(forms.Form): class AddSubStorageForm(forms.Form):
storage_name = forms.CharField(label="storage_name", initial='') storage_name = forms.CharField(label="storage_name", initial='')
responsible = AutocompleteForeingKeyField(api_search_url='user-list', responsible = forms.CharField(label='responsible_user')
image_field_name=None,
name_field_name='username',
foreign_model=get_user_model(),
prepend='@')
is_template = forms.BooleanField(label='is_template', required=False)
class AddSubStorageForm(ChangeStorageForm):
template = AutocompleteForeingKeyField(api_search_url='storage-template-list',
image_field_name=None,
foreign_model=parts_models.Storage,
required=False)
class DeleteStockForm(forms.Form): class DeleteStockForm(forms.Form):
stock_uuid = forms.UUIDField() stock_uuid = forms.UUIDField()
@@ -269,6 +244,7 @@ class AdvancedComponentSearchForm(forms.Form):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_tag = False self.helper.form_tag = False
self.helper.disable_csrf = True
self.helper.layout = Layout( self.helper.layout = Layout(
Row( Row(
Column('name'), Column('name'),
@@ -287,20 +263,53 @@ class AdvancedComponentSearchForm(forms.Form):
), ),
) )
PARAMETER_COMPARISON_TYPES = (
('eq', '=='),
('lte', '<='),
('gte', '>='),
)
class ComponentParameterSearchForm(forms.Form): class ComponentParameterSearchForm(forms.Form):
parameter = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='parameter_name') parameter = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='parameter_name')
value = forms.CharField(max_length=100, required=False) value = forms.CharField(max_length=100, required=False)
compare_method = forms.ChoiceField(choices=PARAMETER_COMPARISON_TYPES, required=True, initial=1)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_tag = False self.helper.form_tag = False
self.helper.disable_csrf = True
self.helper.layout = Layout( self.helper.layout = Layout(
Row( Row(
Column('parameter'), Column('parameter'),
Column('compare_method'),
Column('value') Column('value')
) )
) )
def clean(self):
cleaned_data = super().clean()
parameter = cleaned_data.get('parameter')
value = cleaned_data.get('value')
if value != '' or value != None:
value = value.strip()
if value == '' or value == None:
cleaned_data['value'] = None
value = None
if parameter and value is not None and value != '':
if parameter.parameter_type != 'F':
try:
cleaned_data['value'] = EngineeringNumberConverter.engineering_to_number(value)
except:
raise ValidationError('Cannot convert value to number')
return cleaned_data
class ComponentParameterCreateForm(forms.Form): class ComponentParameterCreateForm(forms.Form):
parameter_type = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='descriptive_name') parameter_type = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='descriptive_name')
value = forms.CharField(required=True, max_length=256) value = forms.CharField(required=True, max_length=256)

View File

@@ -0,0 +1,44 @@
# Generated by Django 3.2.5 on 2022-01-10 18:12
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('parts', '0010_auto_20220103_1606'),
]
operations = [
migrations.AddField(
model_name='componenttype',
name='key_parameter1',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='type_param1', to='parts.componentparametertype'),
),
migrations.AddField(
model_name='componenttype',
name='key_parameter2',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='type_param2', to='parts.componentparametertype'),
),
migrations.AddField(
model_name='componenttype',
name='key_parameter3',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='type_param3', to='parts.componentparametertype'),
),
migrations.CreateModel(
name='PackageParameter',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('value', models.FloatField(default=0)),
('text_value', models.TextField(blank=True)),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='parts.package')),
('parameter_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='parts.componentparametertype')),
],
options={
'ordering': ['id'],
'unique_together': {('package', 'parameter_type')},
},
),
]

View File

@@ -1,24 +0,0 @@
# Generated by Django 3.2.5 on 2024-11-10 12:42
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('parts', '0010_auto_20220103_1606'),
]
operations = [
migrations.AddField(
model_name='storage',
name='is_template',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='storage',
name='template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='template_of', to='parts.storage'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.5 on 2022-11-11 20:18
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('parts', '0011_auto_20220110_1812'),
]
operations = [
migrations.AddField(
model_name='componenttype',
name='parent_class',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='child_classes', to='parts.componenttype'),
),
]

View File

@@ -1,7 +1,7 @@
from django.db import models from django.db import models
from shimatta_modules import RandomFileName from shimatta_modules import RandomFileName
from django.db.models import F, Sum from django.db.models import F, Sum
from django.contrib.auth import get_user_model from django.contrib.auth.models import User as AuthUser
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.dispatch import receiver from django.dispatch import receiver
@@ -47,11 +47,35 @@ class ComponentType(models.Model):
class Meta: class Meta:
ordering = ['class_name'] ordering = ['class_name']
class_name = models.CharField(max_length=50, unique=True) class_name = models.CharField(max_length=50, unique=True)
parent_class = models.ForeignKey('self', on_delete=models.PROTECT, related_name='child_classes', null=True, blank=True)
passive = models.BooleanField() passive = models.BooleanField()
possible_parameter = models.ManyToManyField(ComponentParameterType, blank=True) possible_parameter = models.ManyToManyField(ComponentParameterType, blank=True)
key_parameter1 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param1")
key_parameter2 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param2")
key_parameter3 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param3")
def __str__(self): def __str__(self):
return '[' + self.class_name + ']' return self.get_full_path()
def get_path_components(self):
chain = []
iterator = self
chain.append(self)
while iterator.parent_class is not None:
chain.append(iterator.parent_class)
iterator = iterator.parent_class
return chain
def get_full_path(self):
output = ''
chain = self.get_path_components()
for i in range(len(chain) - 1, -1, -1):
output = output + ' / ' + chain[i].class_name
return output
class Storage(models.Model): class Storage(models.Model):
class Meta: class Meta:
@@ -63,15 +87,7 @@ class Storage(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
name = models.CharField(max_length=100, validators=[storage_name_validator]) name = models.CharField(max_length=100, validators=[storage_name_validator])
parent_storage = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True) parent_storage = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True)
responsible = models.ForeignKey(get_user_model(), on_delete=models.SET_NULL, blank=True, null=True) responsible = models.ForeignKey(AuthUser, on_delete=models.SET_NULL, blank=True, null=True)
# allow storages to be templates which can be selected when adding new storages
is_template = models.BooleanField(default=False)
template = models.ForeignKey('self',
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name='template_of')
def get_path_components(self): def get_path_components(self):
chain = [] chain = []
@@ -152,6 +168,37 @@ class Package(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class PackageParameter(models.Model):
class Meta:
unique_together = ('package', 'parameter_type')
ordering = ['id']
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
package = models.ForeignKey(Package, on_delete=models.CASCADE) # A target package is required!
parameter_type = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE)
value = models.FloatField(default=0)
text_value = models.TextField(null=False, blank=True)
def __str__(self):
if self.parameter_type.parameter_type == 'F':
value = self.text_value
else:
value = str(self.value)
return str(self.package)+ ': '+ str(self.parameter_type) + ': ' + value
def resolved_value_as_string(self):
my_type = self.parameter_type.parameter_type
if my_type == 'E' or my_type == 'I':
# Engineering float number
(num, prefix) = NumConv.number_to_engineering(self.value, it_unit=(True if my_type=='I' else False))
return f'{num:.3f} {prefix}{self.parameter_type.unit}'
elif my_type == 'N':
# Standard float number
return f'{self.value:.3f} {self.parameter_type.unit}'
elif my_type == 'F':
return self.text_value
class Manufacturer(models.Model): class Manufacturer(models.Model):
class Meta: class Meta:
@@ -214,6 +261,33 @@ class Component(models.Model):
sum = 0 sum = 0
return sum return sum
def get_key_parameters(self):
"""
Get the key parameters of a component defined by its component type.
Returns a tuple of 3 elements. All three might be None
"""
p1 = None
p2 = None
p3 = None
if self.component_type:
t = (self.component_type.key_parameter1, self.component_type.key_parameter2, self.component_type.key_parameter3)
if t[0]:
p1 = ComponentParameter.objects.filter(component=self, parameter_type=t[0]).first()
if t[1]:
p2 = ComponentParameter.objects.filter(component=self, parameter_type=t[1]).first()
if t[2]:
p3 = ComponentParameter.objects.filter(component=self, parameter_type=t[2]).first()
return (p1, p2, p3)
def get_key_parameters_as_text(self):
params = self.get_key_parameters()
ret_strings = []
for p in params:
if p:
ret_strings.append(p.resolved_value_as_string())
return ret_strings
class ComponentParameter(models.Model): class ComponentParameter(models.Model):
class Meta: class Meta:
@@ -239,10 +313,12 @@ class ComponentParameter(models.Model):
if my_type == 'E' or my_type == 'I': if my_type == 'E' or my_type == 'I':
# Engineering float number # Engineering float number
(num, prefix) = NumConv.number_to_engineering(self.value, it_unit=(True if my_type=='I' else False)) (num, prefix) = NumConv.number_to_engineering(self.value, it_unit=(True if my_type=='I' else False))
return f'{num:.3f} {prefix}{self.parameter_type.unit}' num = round(num, 3)
return f'{num} {prefix}{self.parameter_type.unit}'
elif my_type == 'N': elif my_type == 'N':
# Standard float number # Standard float number
return f'{self.value:.3f} {self.parameter_type.unit}' num = round(self.value, 3)
return f'{num} {self.parameter_type.unit}'
elif my_type == 'F': elif my_type == 'F':
return self.text_value return self.text_value
@@ -296,7 +372,7 @@ class DistributorNum(models.Model):
class QrPrintJob(models.Model): class QrPrintJob(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, null=False, blank=False) user = models.ForeignKey(AuthUser, on_delete=models.CASCADE, null=False, blank=False)
qrdata = models.CharField(max_length=256, blank=True, null=False) qrdata = models.CharField(max_length=256, blank=True, null=False)
text = models.TextField(max_length=512, blank=True, null=False) text = models.TextField(max_length=512, blank=True, null=False)
print_count = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0), MaxValueValidator(32)]) print_count = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0), MaxValueValidator(32)])
@@ -344,19 +420,3 @@ def auto_delete_file_on_change(sender, instance, **kwargs):
return True return True
if os.path.isfile(old_file.path): if os.path.isfile(old_file.path):
os.remove(old_file.path) os.remove(old_file.path)
@receiver(models.signals.post_save, sender=Storage)
def auto_apply_template_structure(sender, instance, created, **kwargs):
"""
Add the sub-storages from the template.
If there are nested sub-storages these will be added when the sub-storages
are created automatically.
"""
if created:
if instance.template:
for sub_storage in instance.template.storage_set.all():
Storage.objects.create(name=sub_storage.name,
parent_storage=instance,
responsible=instance.responsible,
is_template=False,
template=sub_storage)

View File

@@ -4,6 +4,7 @@ from . import views as parts_views
urlpatterns = [ urlpatterns = [
path('', parts_views.MainView.as_view(), name='parts-main'), path('', parts_views.MainView.as_view(), name='parts-main'),
path('components/', parts_views.ComponentView.as_view(), name='parts-components'), path('components/', parts_views.ComponentView.as_view(), name='parts-components'),
path('componenttypes/', parts_views.ComponentTypeView.as_view(), name='parts-componenttypes'),
path('packages/', parts_views.PackageView.as_view(), name='parts-packages'), path('packages/', parts_views.PackageView.as_view(), name='parts-packages'),
path('distributors/', parts_views.DistributorView.as_view(), name='parts-distributors'), path('distributors/', parts_views.DistributorView.as_view(), name='parts-distributors'),
path('stocks/', parts_views.StockView.as_view(), name='parts-stocks'), path('stocks/', parts_views.StockView.as_view(), name='parts-stocks'),
@@ -16,4 +17,5 @@ urlpatterns = [
path('distributors/<slug:uuid>/', parts_views.DistributorDetailView.as_view(), name='parts-distributors-detail'), path('distributors/<slug:uuid>/', parts_views.DistributorDetailView.as_view(), name='parts-distributors-detail'),
path('manufacturers/', parts_views.ManufacturersViewSet.as_view(), name='parts-manufacturers'), path('manufacturers/', parts_views.ManufacturersViewSet.as_view(), name='parts-manufacturers'),
path("manufacturers/<slug:uuid>/", parts_views.ManufacturerDetailViewSet.as_view(), name='parts-manufacturers-detail'), path("manufacturers/<slug:uuid>/", parts_views.ManufacturerDetailViewSet.as_view(), name='parts-manufacturers-detail'),
path("componenttypes/<slug:uuid>/", parts_views.ComponentTypeDetailView.as_view(), name='parts-componenttypes-detail'),
] ]

View File

@@ -1,14 +1,20 @@
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.urls import reverse from django.urls import resolve, reverse
from django.contrib.auth import logout, login from django.contrib.auth import logout, login
from django.contrib.auth.models import User
from django.utils.http import urlencode
from django.http import HttpResponse
from .navbar import NavBar from .navbar import NavBar
from django.contrib.auth.forms import AuthenticationForm as AuthForm from django.contrib.auth.forms import AuthenticationForm as AuthForm
from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import update_session_auth_hash
from django.views import View
import django.forms as forms import django.forms as forms
from django.views.generic import TemplateView, DetailView from django.views.generic import TemplateView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from .models import Storage, Stock, Component, Distributor, Manufacturer, Package, ComponentParameter, ComponentParameterType, DistributorNum from .models import Storage, Stock, Component, Distributor, Manufacturer, Package
from .models import ComponentParameter, ComponentParameterType, DistributorNum, PackageParameter
from .models import ComponentType
from .qr_parser import QrCodeValidator from .qr_parser import QrCodeValidator
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -16,11 +22,12 @@ from django.db import IntegrityError
from django.db.models import ProtectedError from django.db.models import ProtectedError
from .forms import * from .forms import *
from django.db.models import Q from django.db.models import Q
from django.db.models import Prefetch
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.forms import formset_factory from django.forms import formset_factory
import uuid import uuid
ParameterSearchFormSet = formset_factory(ComponentParameterSearchForm, extra=1) ParameterSearchFormSet = formset_factory(ComponentParameterSearchForm, extra=0)
class QrSearchForm(forms.Form): class QrSearchForm(forms.Form):
my_qr_validator = QrCodeValidator() my_qr_validator = QrCodeValidator()
@@ -31,6 +38,15 @@ class QrSearchForm(forms.Form):
qr_search = forms.CharField(label='qr_search', validators=[my_qr_validator]) qr_search = forms.CharField(label='qr_search', validators=[my_qr_validator])
class KeepSearchParamMixin(object):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
search = self.request.GET.get('search', default=None)
if search:
context['additional_params'] = urlencode({'search': search})
return context
class BaseTemplateMixin(object): class BaseTemplateMixin(object):
navbar_selected = '' navbar_selected = ''
base_title = '' base_title = ''
@@ -139,26 +155,78 @@ def login_view(request):
# Create your views here. # Create your views here.
class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): class ComponentTypeView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/component-types.html'
base_title = 'Component Types'
default_page_size = 25
def filter_queryset(self, queryset, search_string):
if search_string is None or search_string == '':
return queryset
search_fragments = search_string.strip().split()
for search in search_fragments:
queryset = queryset.filter(Q(class_name__icontains = search))
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
search = self.request.GET.get('search', default=None)
page_num = self.request.GET.get('page', default=1)
context['search_string'] = search
queryset = ComponentType.objects.all()
types = self.filter_queryset(queryset, search)
comptypes = Paginator(types, self.default_page_size)
context['comptypes'] = comptypes.get_page(page_num)
return context
def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)
class ComponentTypeDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
model = ComponentType
base_title = ''
pk_url_kwarg = 'uuid'
template_name = 'parts/component-types-detail.html'
def get_breadcrumbs(self):
crumbs = self.object.get_path_components()
# Reverse list and drop the last element of the reversed list
crumbs = crumbs[::-1][:-1]
return crumbs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['breadcrumbs'] = self.get_breadcrumbs()
return context
class ComponentView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/components.html' template_name = 'parts/components.html'
base_title = 'Components' base_title = 'Components'
navbar_selected = 'Components' navbar_selected = 'Components'
default_page_size = 25 default_page_size = 25
def get_component_query_set(self, search_string): def get_component_query_set(self, search_string):
queryset = Component.objects.all() queryset = Component.objects.select_related('package', 'manufacturer', 'component_type').prefetch_related('componentparameter_set').all()
if not search_string: if search_string is None or search_string == '':
return queryset return queryset
search_fragments = search_string.strip().split() search_fragments = search_string.strip().split()
for search in search_fragments: for search in search_fragments:
queryset = queryset.filter(Q(name__icontains = search) | Q(manufacturer__name__icontains = search) | Q(package__name__icontains = search)) queryset = queryset.filter(Q(name__icontains = search) | Q(manufacturer__name__icontains = search) | Q(package__name__icontains = search))
return queryset return queryset
def get_component_queryset_from_advanced_search(self, cleaned_data): def get_component_queryset_from_advanced_search(self, cleaned_data):
queryset = Component.objects.all() queryset = Component.objects.select_related('manufacturer', 'package', 'component_type').prefetch_related('componentparameter_set').all()
if cleaned_data['name']: if cleaned_data['name']:
queryset = queryset.filter(Q(name__icontains=cleaned_data['name'])) queryset = queryset.filter(Q(name__icontains=cleaned_data['name']))
@@ -178,6 +246,21 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
queryset = queryset.filter(manufacturer=cleaned_data['manufacturer']) queryset = queryset.filter(manufacturer=cleaned_data['manufacturer'])
return queryset return queryset
def filter_queryset_with_parameters(self, queryset, parameter, value, compare_method):
if parameter and (value is None or value == ''):
return queryset.filter(Q(componentparameter__parameter_type=parameter))
elif parameter and value is not None:
if parameter.parameter_type == 'F':
return queryset.filter(Q(componentparameter__text_value__icontains=value) & Q(componentparameter__parameter_type=parameter))
else:
if compare_method == 'lte': # <=
return queryset.filter(Q(componentparameter__value__lte=value) & Q(componentparameter__parameter_type=parameter))
elif compare_method == 'gte': # >=
return queryset.filter(Q(componentparameter__value__gte=value) & Q(componentparameter__parameter_type=parameter))
else:
return queryset.filter(Q(componentparameter__value=value) & Q(componentparameter__parameter_type=parameter))
return queryset
def get_context_data_int(self, advanced_search, parameter_formset : ParameterSearchFormSet, **kwargs): def get_context_data_int(self, advanced_search, parameter_formset : ParameterSearchFormSet, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@@ -191,11 +274,14 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
if advanced_search.is_valid(): if advanced_search.is_valid():
paginator_queryset = self.get_component_queryset_from_advanced_search(advanced_search.cleaned_data) paginator_queryset = self.get_component_queryset_from_advanced_search(advanced_search.cleaned_data)
else: else:
paginator_queryset = Component.objects.all() paginator_queryset = self.get_component_query_set(None)
# Process parameters
for f in parameter_formset:
# If the form is valid and has changed compared to its initial empty state
if f.is_valid() and f.has_changed():
paginator_queryset = self.filter_queryset_with_parameters(paginator_queryset, f.cleaned_data['parameter'], f.cleaned_data['value'], f.cleaned_data['compare_method'])
if parameter_formset.is_valid():
# Process parameters
pass
else: else:
search = self.request.GET.get('search', default=None) search = self.request.GET.get('search', default=None)
@@ -209,15 +295,24 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
if not parameter_formset: if not parameter_formset:
context['advanced_search_param_formset'] = ParameterSearchFormSet() context['advanced_search_param_formset'] = ParameterSearchFormSet()
if not advanced_search: if not advanced_search:
context['advanced_search_form'] = AdvancedComponentSearchForm(auto_id='adv_search_%s') context['advanced_search_form'] = AdvancedComponentSearchForm(auto_id='adv_search_%s')
return context return context
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return self.get_context_data_int(advanced_search = None, parameter_formset=None, **kwargs) adv_search_form = None
adv_param_search_formset = None
if 'submit-advanced-search' in self.request.GET:
adv_search_form = AdvancedComponentSearchForm(auto_id='adv_search_%s', data=self.request.GET)
adv_param_search_formset = ParameterSearchFormSet(data=self.request.GET)
if adv_search_form.is_valid():
pass
if adv_param_search_formset.is_valid():
pass
return self.get_context_data_int(advanced_search = adv_search_form, parameter_formset=adv_param_search_formset, **kwargs)
def handle_new_component_post(self, request, open=False, **kwargs): def handle_new_component_post(self, request, open=False, **kwargs):
@@ -234,32 +329,15 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
return redirect(reverse('parts-components-detail', kwargs={'uuid':new_component.id})) return redirect(reverse('parts-components-detail', kwargs={'uuid':new_component.id}))
return self.render_to_response(context) return self.render_to_response(context)
def handle_advanced_search_post(self, request, **kwargs):
form = AdvancedComponentSearchForm(auto_id='adv_search_%s', data=request.POST)
param_formset = ParameterSearchFormSet(data=request.POST)
if form.is_valid():
print('Valid')
if param_formset.is_valid():
print('Formset is valid!')
context = self.get_context_data_int(form, param_formset, **kwargs)
return self.render_to_response(context)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if 'submit-edit-component' in request.POST: if 'submit-edit-component' in request.POST:
return self.handle_new_component_post(request, open=False, **kwargs) return self.handle_new_component_post(request, open=False, **kwargs)
elif 'submit-edit-component-open' in request.POST: elif 'submit-edit-component-open' in request.POST:
return self.handle_new_component_post(request, open=True, **kwargs) return self.handle_new_component_post(request, open=True, **kwargs)
elif 'submit-advanced-search' in request.POST:
return self.handle_advanced_search_post(request, **kwargs)
else: else:
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): class PackageView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/packages.html' template_name = 'parts/packages.html'
base_title = 'Packages' base_title = 'Packages'
navbar_selected = 'Packages' navbar_selected = 'Packages'
@@ -321,7 +399,7 @@ class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
class DistributorView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): class DistributorView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/distributors.html' template_name = 'parts/distributors.html'
base_title = 'Distributors' base_title = 'Distributors'
navbar_selected = 'Distributors' navbar_selected = 'Distributors'
@@ -397,23 +475,36 @@ class StockView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
context['low_stocks'] = low_stock_paginator.get_page(low_stock_page) context['low_stocks'] = low_stock_paginator.get_page(low_stock_page)
context['storages'] = storage_paginator.get_page(storage_page) context['storages'] = storage_paginator.get_page(storage_page)
add_stor_form = AddSubStorageForm() add_stor_form = AddSubStorageForm()
add_stor_form.fields['responsible'].initial = self.request.user.id add_stor_form.fields['responsible'].initial = self.request.user.username
context['add_storage_form'] = add_stor_form context['add_storage_form'] = add_stor_form
return context return context
def handle_add_storage(self, request, **kwargs): def handle_add_storage(self, request, **kwargs):
return_invalid_form = False
f = AddSubStorageForm(data=request.POST) f = AddSubStorageForm(data=request.POST)
if f.is_valid(): if f.is_valid():
sub_name = f.cleaned_data['storage_name'] new_storage_name = f.cleaned_data['storage_name']
try: try:
Storage.objects.create(name=sub_name, resp_user = User.objects.get(username=f.cleaned_data['responsible'])
responsible=f.cleaned_data['responsible'], except Exception as _:
is_template=f.cleaned_data['is_template'], resp_user = None
template=f.cleaned_data.get('template')) f.add_error('responsible', 'Invalid Responsible User')
except ValidationError as v_err: return_invalid_form = True
f.add_error('storage_name', '. '.join(v_err.messages))
if resp_user is not None:
try:
Storage.objects.create(name=new_storage_name, responsible=resp_user, parent_storage=None)
except ValidationError as verr:
return_invalid_form = True
f.add_error('storage_name', ' .'.join(verr.messages))
else:
return_invalid_form = True
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
context['add_storage_form'] = f if return_invalid_form:
context['add_storage_form'] = f
return self.render_to_response(context) return self.render_to_response(context)
def post(self, request, **kwargs): def post(self, request, **kwargs):
@@ -422,7 +513,7 @@ class StockView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
return super().post(request, **kwargs) return super().post(request, **kwargs)
class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView): class StockViewDetail(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, DetailView):
template_name = 'parts/stocks-detail.html' template_name = 'parts/stocks-detail.html'
model = Storage model = Storage
pk_url_kwarg = 'uuid' pk_url_kwarg = 'uuid'
@@ -480,16 +571,10 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
context['stocks'] = stocks context['stocks'] = stocks
context['stock_search'] = stock_search_input context['stock_search'] = stock_search_input
add_storage_form = AddSubStorageForm() add_storage_form = AddSubStorageForm()
add_storage_form.fields['responsible'].initial = self.request.user.id add_storage_form.fields['responsible'].initial = self.request.user.username
context['add_storage_form'] = add_storage_form context['add_storage_form'] = add_storage_form
change_storage_form = ChangeStorageForm()
change_storage_form.fields['storage_name'].initial = self.object.name
change_storage_form.fields['responsible'].initial = self.object.responsible.id
change_storage_form.fields['is_template'].initial = self.object.is_template
context['change_storage_form'] = change_storage_form
context['delete_storage_error'] = None context['delete_storage_error'] = None
context['add_stock_form'] = AddStockForm() context['add_stock_form'] = AddStockForm()
return context return context
def handle_add_storage_post(self, request, **kwargs): def handle_add_storage_post(self, request, **kwargs):
@@ -497,32 +582,17 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
if f.is_valid(): if f.is_valid():
sub_name = f.cleaned_data['storage_name'] sub_name = f.cleaned_data['storage_name']
try: try:
Storage.objects.create(name=sub_name, user = User.objects.get(username=f.cleaned_data['responsible'])
parent_storage=self.object, try:
responsible=f.cleaned_data['responsible'], Storage.objects.create(name=sub_name, parent_storage=self.object, responsible=user)
is_template=f.cleaned_data['is_template'], except ValidationError as v_err:
template=f.cleaned_data.get('template')) f.add_error('storage_name', '. '.join(v_err.messages))
except ValidationError as v_err: except:
f.add_error('storage_name', '. '.join(v_err.messages)) f.add_error('responsible', 'Invalid user')
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
context['add_storage_form'] = f context['add_storage_form'] = f
return self.render_to_response(context) return self.render_to_response(context)
def handle_change_storage_post(self, request, **kwargs):
f = ChangeStorageForm(data=request.POST)
if f.is_valid():
sub_name = f.cleaned_data['storage_name']
try:
self.object.name = f.cleaned_data['storage_name']
self.object.responsible = f.cleaned_data['responsible']
self.object.is_template = f.cleaned_data['is_template']
self.object.save()
except ValidationError as v_err:
f.add_error('storage_name', '. '.join(v_err.messages))
context = self.get_context_data(**kwargs)
context['change_storage_form'] = f
return self.render_to_response(context)
def handle_del_storage_post(self, request, **kwargs): def handle_del_storage_post(self, request, **kwargs):
parent = self.object.parent_storage parent = self.object.parent_storage
try: try:
@@ -536,7 +606,6 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
return redirect('parts-stocks') return redirect('parts-stocks')
else: else:
return redirect(reverse('parts-stocks-detail', kwargs={'uuid':parent.id})) return redirect(reverse('parts-stocks-detail', kwargs={'uuid':parent.id}))
def handle_del_stock_post(self, request, **kwargs): def handle_del_stock_post(self, request, **kwargs):
del_error = None del_error = None
if 'stock_uuid' in request.POST: if 'stock_uuid' in request.POST:
@@ -562,9 +631,10 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
if edit_form.is_valid(): if edit_form.is_valid():
edit_form.save() edit_form.save()
else: else:
pass # Todo: Handle error pass
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
return self.render_to_response(context) return self.render_to_response(context)
def handle_amount_change_post(self, request, increase, **kwargs): def handle_amount_change_post(self, request, increase, **kwargs):
@@ -598,8 +668,6 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
if 'submit-add-storage' in request.POST: if 'submit-add-storage' in request.POST:
return self.handle_add_storage_post(request, **kwargs) return self.handle_add_storage_post(request, **kwargs)
if 'submit-change-storage' in request.POST:
return self.handle_change_storage_post(request, **kwargs)
elif 'submit-delete-storage' in request.POST: elif 'submit-delete-storage' in request.POST:
return self.handle_del_storage_post(request, **kwargs) return self.handle_del_storage_post(request, **kwargs)
elif 'submit-delete-stock' in request.POST: elif 'submit-delete-stock' in request.POST:
@@ -617,7 +685,7 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
template_name = 'parts/components-detail.html' template_name = 'parts/components-detail.html'
model = Component queryset = Component.objects.select_related('component_type', 'package', 'manufacturer').prefetch_related('componentparameter_set', 'distributornum_set')
pk_url_kwarg = 'uuid' pk_url_kwarg = 'uuid'
base_title = '' base_title = ''
navbar_selected = 'Components' navbar_selected = 'Components'
@@ -630,8 +698,13 @@ class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
context['comp_form'] = ComponentForm(instance=self.object) context['comp_form'] = ComponentForm(instance=self.object)
context['new_distri_num_form'] = DistributorNumberCreateForm() context['new_distri_num_form'] = DistributorNumberCreateForm()
context['new_param_form'] = ComponentParameterCreateForm() context['new_param_form'] = ComponentParameterCreateForm()
context['distri_nums'] = DistributorNum.objects.filter(component=self.object).order_by('distributor__name') context['distri_nums'] = self.object.distributornum_set.select_related('distributor').order_by('distributor__name')
context['parameters'] = ComponentParameter.objects.filter(component=self.object).order_by('parameter_type__parameter_name') context['parameters'] = self.object.componentparameter_set.order_by('parameter_type__parameter_name')
if self.object.package:
context['package_parameters'] = PackageParameter.objects.filter(package=self.object.package).order_by('parameter_type__parameter_name')
parameter_texts = self.object.get_key_parameters_as_text()
context['key_parameter_string'] = ', '.join(parameter_texts)
return context return context
@@ -841,7 +914,7 @@ class DistributorDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
class ManufacturersViewSet(LoginRequiredMixin, BaseTemplateMixin, TemplateView): class ManufacturersViewSet(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/manufacturers.html' template_name = 'parts/manufacturers.html'
base_title = 'Manufacturers' base_title = 'Manufacturers'
navbar_selected = 'Manufacturers' navbar_selected = 'Manufacturers'

View File

@@ -8,6 +8,23 @@ https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/ https://docs.djangoproject.com/en/3.2/ref/settings/
Development mode is selected by setting env variable
- DJANGO_DEV_MODE
The following environment variables have to be set for Production Mode but might be optional for dev mode:
- DJANGO_SECRET_KEY
- DJANGO_ALLOWED_HOST
- DJANGO_STATIC_ROOT
- DJANGO_MEDIA_URL
- DJANGO_MEDIA_ROOT
- DJANGO_POSTGRESQL_SOCKET
The following can be set
- DJANGO_SECURE_HSTS_SECONDS (defaults to 120)
""" """
from pathlib import Path from pathlib import Path
@@ -16,17 +33,34 @@ import os
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
def get_env_value(env_variable, default=None):
try:
return os.environ[env_variable]
except KeyError:
if default is not None:
return default
error_msg = 'Set the {} environment variable'.format(env_variable)
raise Exception(error_msg)
RUNS_IN_DEV_MODE = True if get_env_value('DJANGO_DEV_MODE', default=False) != False else False
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-vq_@ue3ul@&4bz7wkcpf3pjrwf8o$7g!z-rw$ftr-$)7l3*m=^' SECRET_KEY=''
if RUNS_IN_DEV_MODE:
SECRET_KEY = get_env_value('DJANGO_SECRET_KEY',
default='django-insecure-vq_@ue3ul@&4bz7wkcpf3pjrwf8o$7g!z-rw$ftr-$)7l3*m=^')
else:
SECRET_KEY = get_env_value('DJANGO_SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True if RUNS_IN_DEV_MODE else False
ALLOWED_HOSTS = ['localhost'] ALLOWED_HOSTS = ['localhost']
if not RUNS_IN_DEV_MODE:
ALLOWED_HOSTS = ['localhost', get_env_value('DJANGO_ALLOWED_HOST')]
# Application definition # Application definition
@@ -96,10 +130,34 @@ WSGI_APPLICATION = 'shimatta_kenkyusho.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases # https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DATABASES = { DATABASES = {}
if RUNS_IN_DEV_MODE:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
else:
b_pw = ''
try:
db_pw = get_env_value('DJANGO_POSTGRESQL_PW')
except:
pass
db_user = ''
try:
db_user = get_env_value('DJANGO_POSTGRESQL_USER')
except:
pass
DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.postgresql',
'NAME': BASE_DIR / 'db.sqlite3', 'NAME': 'shimatta_kenkyusho',
'USER': db_user,
'PASSWORD': db_pw,
'HOST': get_env_value('DJANGO_POSTGRESQL_SOCKET'),
} }
} }
@@ -167,12 +225,17 @@ STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"), os.path.join(BASE_DIR, "static"),
] ]
STATIC_ROOT = None
if not RUNS_IN_DEV_MODE:
STATIC_ROOT = get_env_value('DJANGO_STATIC_ROOT')
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media") MEDIA_URL = get_env_value('DJANGO_MEDIA_URL', default='/media/')
MEDIA_ROOT = get_env_value('DJANGO_MEDIA_ROOT', default=os.path.join(BASE_DIR, "media"))
LOGIN_URL = '/login' LOGIN_URL = '/login'
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
@@ -183,3 +246,16 @@ SHIMATTA_KENKYUSHO_TITLE = 'しまった・研究所'
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5" CRISPY_TEMPLATE_PACK = "bootstrap5"
# Production only settings
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = get_env_value('DJANGO_SECURE_HSTS_SECONDS', default=120)
if RUNS_IN_DEV_MODE:
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
SECURE_SSL_REDIRECT = False

View File

@@ -1,235 +0,0 @@
"""
Django settings for shimatta_kenkyusho project.
Generated by 'django-admin startproject' using Django 3.2.5.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
The following environment variables have to be set:
- DJANGO_SECRET_KEY
- DJANGO_ALLOWED_HOST
- DJANGO_STATIC_ROOT
- DJANGO_MEDIA_URL
- DJANGO_MEDIA_ROOT
- DJANGO_POSTGRESQL_SOCKET
The following can be set
- DJANGO_SECURE_HSTS_SECONDS (defaults to 120)
"""
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
def get_env_value(env_variable, default=None):
try:
return os.environ[env_variable]
except KeyError:
if default is not None:
return default
error_msg = 'Set the {} environment variable'.format(env_variable)
raise Exception(error_msg)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = get_env_value('DJANGO_SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = ['localhost', get_env_value('DJANGO_ALLOWED_HOST')]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'parts.apps.PartsConfig',
'api.apps.ApiConfig',
'rest_framework.authtoken',
'django_filters',
'qr_code',
'rest_framework',
'crispy_forms',
'crispy_bootstrap5',
'django.forms',
]
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
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 = 'shimatta_kenkyusho.urls'
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
'qr-code': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'qr-code-cache',
'TIMEOUT': 3600
}
}
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(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 = 'shimatta_kenkyusho.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
db_pw = ''
try:
db_pw = get_env_value('DJANGO_POSTGRESQL_PW')
except:
pass
db_user = ''
try:
db_user = get_env_value('DJANGO_POSTGRESQL_USER')
except:
pass
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'shimatta_kenkyusho',
'USER': db_user,
'PASSWORD': db_pw,
'HOST': get_env_value('DJANGO_POSTGRESQL_SOCKET'),
'PORT': get_env_value('DJANGO_POSTGRESQL_PORT'),
}
}
# Password validation
# https://docs.djangoproject.com/en/3.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',
},
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'api.ExpiringAuthToken.ExpiringTokenAuthentication',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 10,
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour',
'user': '2000/hour'
}
}
REST_FRAMEWORK_TOKEN_EXPIRE_HOURS = 4
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
STATIC_ROOT = get_env_value('DJANGO_STATIC_ROOT')
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
MEDIA_URL = get_env_value('DJANGO_MEDIA_URL')
MEDIA_ROOT = get_env_value('DJANGO_MEDIA_ROOT')
LOGIN_URL = '/login'
LOGIN_REDIRECT_URL = '/'
SHIMATTA_KENKYUSHO_TITLE = 'しまった・研究所'
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
# Production only settings
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = False
SECURE_HSTS_SECONDS = get_env_value('DJANGO_SECURE_HSTS_SECONDS', default=120)

View File

@@ -14,14 +14,13 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include, re_path
from django.conf.urls import url
from django.conf.urls.static import static from django.conf.urls.static import static
from django.conf import settings from django.conf import settings
from parts import views as parts_views from parts import views as parts_views
urlpatterns = [ urlpatterns = [
url(r'^admin/login/', parts_views.login_view), re_path(r'^admin/login/', parts_views.login_view),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/v1/', include('api.urls'), name='api-root'), path('api/v1/', include('api.urls'), name='api-root'),
path('', include('parts.urls')), path('', include('parts.urls')),

View File

@@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shimatta_kenkyusho.settings_production') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shimatta_kenkyusho.settings')
application = get_wsgi_application() application = get_wsgi_application()

View File

@@ -1,67 +1,67 @@
class EngineeringNumberConverter(): class EngineeringNumberConverter():
prefixes = [ prefixes = [
('y', 1e-24), ('y', 1e-24),
('z', 1e-21), ('z', 1e-21),
('a', 1e-18), ('a', 1e-18),
('f', 1e-15), ('f', 1e-15),
('p', 1e-12), ('p', 1e-12),
('n', 1e-9), ('n', 1e-9),
('u', 1e-6), ('u', 1e-6),
('m', 1e-3), ('m', 1e-3),
# We skip centi and dezi because no one really uses these besides for length measurements # We skip centi and dezi because no one really uses these besides for length measurements
('', 1), ('', 1),
# We also skip h for hekto # We also skip h for hekto
('k', 1e3), ('k', 1e3),
('M', 1e6), ('M', 1e6),
('G', 1e9), ('G', 1e9),
('T', 1e12), ('T', 1e12),
('P', 1e15), ('P', 1e15),
('E', 1e18), ('E', 1e18),
('Z', 1e21), ('Z', 1e21),
('Y', 1e24), ('Y', 1e24),
] ]
it_prefixes = [ it_prefixes = [
('', 1), ('', 1),
('Ki', 1024), ('Ki', 1024),
('Mi', 1024*1024), ('Mi', 1024*1024),
('Gi', 1024*1024*1024), ('Gi', 1024*1024*1024),
('Ti', 1024*1024*1024*1024) ('Ti', 1024*1024*1024*1024)
] ]
@classmethod @classmethod
def number_to_engineering(c, number, it_unit = False): def number_to_engineering(c, number, it_unit=False):
""" """
Convert a number to engineering SI syntax with prefix. Convert a number to engineering SI syntax with prefix.
This function will return a tuple of (new_number, prefix) This function will return a tuple of (new_number, prefix)
""" """
if it_unit: if it_unit:
used_prefixes = c.it_prefixes used_prefixes = c.it_prefixes
else: else:
used_prefixes = c.prefixes used_prefixes = c.prefixes
if (len(used_prefixes) < 2): if (len(used_prefixes) < 2):
return (number / used_prefixes[0][1], used_prefixes[0]) return (number / used_prefixes[0][1], used_prefixes[0])
for i, (prefix, scale) in enumerate(used_prefixes[1:], 1): for i, (prefix, scale) in enumerate(used_prefixes[1:], 1):
if number < scale: if number < scale:
return (number / used_prefixes[i-1][1], used_prefixes[i-1][0]) return (number / used_prefixes[i-1][1], used_prefixes[i-1][0])
return (number / used_prefixes[-1][1], used_prefixes[-1][0]) return (number / used_prefixes[-1][1], used_prefixes[-1][0])
@classmethod @classmethod
def engineering_to_number(c, input): def engineering_to_number(c, input):
cleaned_input = input.strip().replace(' ', '') cleaned_input = input.strip().replace(' ', '')
selected_scaling = 1 selected_scaling = 1
for (prefix, scale) in c.prefixes+c.it_prefixes: for (prefix, scale) in c.prefixes+c.it_prefixes:
if prefix == '': if prefix == '':
continue continue
if cleaned_input.endswith(prefix): if cleaned_input.endswith(prefix):
cleaned_input = cleaned_input.replace(prefix, '') cleaned_input = cleaned_input.replace(prefix, '')
selected_scaling = scale selected_scaling = scale
break break
return float(cleaned_input) * selected_scaling return float(cleaned_input) * selected_scaling

View File

@@ -12,9 +12,9 @@ function initialize_autocompletion_foreign_key_field(search_element) {
var name_field_name = search_element.getAttribute('data-ac-name-field'); var name_field_name = search_element.getAttribute('data-ac-name-field');
var search_url = search_element.getAttribute('data-ac-url'); var search_url = search_element.getAttribute('data-ac-url');
var base_id = search_element.getAttribute('id'); var base_id = search_element.getAttribute('id');
var uuid_field = search_element.parentElement.parentElement.querySelector('#'+base_id+'-uuid-field'); var uuid_field = search_element.parentElement.querySelector('#'+base_id+'-uuid-field');
var dflex_container = search_element.parentElement.parentElement.querySelector('#'+base_id+'-dflex-container'); var dflex_container = search_element.parentElement.querySelector('#'+base_id+'-dflex-container');
var initial_delete_button = search_element.parentElement.parentElement.querySelector('[data-ac-delete]'); var initial_delete_button = search_element.parentElement.querySelector('[data-ac-delete]');
console.log(initial_delete_button); console.log(initial_delete_button);
console.log(image_field_name); console.log(image_field_name);

View File

@@ -29,10 +29,6 @@ function api_search_user(search, onSuccess, onFail) {
return api_ajax_request_without_send('GET', api_urls_v1['user-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail); return api_ajax_request_without_send('GET', api_urls_v1['user-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail);
} }
function api_search_storage_template(search, onSuccess, onFail) {
return api_ajax_request_without_send('GET', api_urls_v1['storage-template-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail);
}
function api_search_component(search, onSuccess, onFail) { function api_search_component(search, onSuccess, onFail) {
return api_ajax_request_without_send('GET', api_urls_v1['component-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail); return api_ajax_request_without_send('GET', api_urls_v1['component-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail);
} }

View File

@@ -65,7 +65,6 @@
'user-list': '{% url 'user-list' %}', 'user-list': '{% url 'user-list' %}',
'groups-list': '{% url 'user-list' %}', 'groups-list': '{% url 'user-list' %}',
'storage-list': '{% url 'storage-list' %}', 'storage-list': '{% url 'storage-list' %}',
'storage-template-list': '{% url 'storage-template-list' %}',
'component-list': '{% url 'component-list' %}', 'component-list': '{% url 'component-list' %}',
'package-list': '{% url 'package-list' %}', 'package-list': '{% url 'package-list' %}',
'stock-list': '{% url 'stock-list' %}', 'stock-list': '{% url 'stock-list' %}',

View File

@@ -2,21 +2,21 @@
<nav aria-label="{{aria_label}}"> <nav aria-label="{{aria_label}}">
<ul class="pagination"> <ul class="pagination">
{% if paginator.has_previous %} {% if paginator.has_previous %}
<li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.previous_page_number}}">&laquo;</a></li> <li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.previous_page_number}}{% if additional_params %}&{{additional_params}}{% endif %}">&laquo;</a></li>
{% else %} {% else %}
<li class="page-item disabled"><span class="page-link">&laquo;</span></li> <li class="page-item disabled"><span class="page-link">&laquo;</span></li>
{% endif %} {% endif %}
{% for i in paginator.paginator.page_range %} {% for i in paginator.paginator.page_range %}
{% if i <= paginator.number|add:5 and i >= paginator.number|add:-5 %} {% if i <= paginator.number|add:5 and i >= paginator.number|add:-5 %}
{% if i == paginator.number %} {% if i == paginator.number %}
<li class="page-item active"><a class="page-link" href="?{{get_param}}={{i}}">{{i}}</a></li> <li class="page-item active"><a class="page-link" href="?{{get_param}}={{i}}{% if additional_params %}&{{additional_params}}{% endif %}">{{i}}</a></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="?{{get_param}}={{i}}">{{i}}</a></li> <li class="page-item"><a class="page-link" href="?{{get_param}}={{i}}{% if additional_params %}&{{additional_params}}{% endif %}">{{i}}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if paginator.has_next %} {% if paginator.has_next %}
<li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.next_page_number}}">&raquo;</a></li> <li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.next_page_number}}{% if additional_params %}&{{additional_params}}{% endif %}">&raquo;</a></li>
{% else %} {% else %}
<li class="page-item disabled"><span class="page-link">&raquo;</span></li> <li class="page-item disabled"><span class="page-link">&raquo;</span></li>
{% endif %} {% endif %}

View File

@@ -0,0 +1,27 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container">
<nav aria-label="breadcrumb" class="fs-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"></li>
{% for crumb in breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'parts-componenttypes-detail' uuid=crumb.id %}">{{crumb.class_name}}</a></li>
{% endfor %}
<li class="breadcrumb-item active" aria-current="page">{{object.class_name}}</li>
</ol>
</nav>
<div class="row">
<div class="col-md">
<h2>Component Type: {{object.class_name}}</h2>
</div>
</div>
</div>
{% endblock content %}
{% block custom_scripts %}
<script type="text/javascript">
</script>
{% endblock custom_scripts %}

View File

@@ -0,0 +1,46 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md">
<h2>Component Types</h2>
<form action="" method="get">
<div class="input-group mb-3">
<input class="form-control" name="search" type="search" placeholder="Search Component Type..." {% if search_string %}value="{{search_string}}"{% endif %}>
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i>
</button>
</div>
</form>
{% include 'paginator.html' with paginator=comptypes get_param='page' aria_label='Component Type Page Navigation' %}
<div class="list-group mb-3">
{% for t in comptypes %}
<a href="{% url 'parts-componenttypes-detail' uuid=t.id %}" class="text-decoration-none">
<li class="list-group-item list-group-item-action d-flex flex-row align-items-center justify-content-between">
<div class="p-2">
{{t}}
</div>
{% if t.passive %}
<div class="p-2">
<span class="text-muted">&nbsp;passive</span>
</div>
{% endif %}
</li>
</a>
{% endfor %}
</div>
{% include 'paginator.html' with paginator=comptypes get_param='page' aria_label='Component Type Page Navigation' %}
</div>
</div>
</div>
{% endblock content %}
{% block custom_scripts %}
<script type="text/javascript">
</script>
{% endblock custom_scripts %}

View File

@@ -39,7 +39,7 @@
<tbody> <tbody>
<tr> <tr>
<td class="align-middle" scope="row"> <td class="align-middle" scope="row">
{{component.name}} {{component.name}}{% if key_parameter_string %}<br><span class="text-secondary">{{key_parameter_string}}</span>{% endif %}
</td> </td>
<td class="align-middle" > <td class="align-middle" >
{% if component.package %} {% if component.package %}
@@ -119,6 +119,17 @@
<th scope="col"></th> <th scope="col"></th>
</thead> </thead>
<tbody> <tbody>
{% for param in package_parameters %}
<td>
<h6 {% if param.parameter_type.parameter_description %} class="accordion-header" data-bs-toggle="collapse" data-bs-target="#collapse-pkg-parameter-desc-{{forloop.counter}}"{% endif %}>
{{param.parameter_type.parameter_name}}
</h6>
</td>
<td>
{{param.resolved_value_as_string}}
</td>
<td><span class="text-secondary">from Package</span></td>
{% endfor %}
{% for param in parameters %} {% for param in parameters %}
<tr> <tr>
<td> <td>
@@ -148,6 +159,13 @@
</div> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% for param in package_parameters %}
{% if param.parameter_type.parameter_description %}
<div class="collapse accordion-collapse" id="collapse-pkg-parameter-desc-{{forloop.counter}}" data-bs-parent="#accordion-param-desc">
{{param.parameter_type.parameter_description}}
</div>
{% endif %}
{% endfor %}
</div> </div>
</div> </div>
<div class="col"> <div class="col">

View File

@@ -17,7 +17,7 @@
</div> </div>
</form> </form>
<div class="collapse mb-3{% if advanced_search_shown %} show{% endif %}" id="advanced-search-collapse" aria-expanded="{% if advanced_search_shown %}true{% else %}false{% endif %}"> <div class="collapse mb-3{% if advanced_search_shown %} show{% endif %}" id="advanced-search-collapse" aria-expanded="{% if advanced_search_shown %}true{% else %}false{% endif %}">
<form method="POST"> <form method="GET">
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
{% crispy advanced_search_form %} {% crispy advanced_search_form %}
@@ -31,6 +31,9 @@
<input type="submit" name="submit-advanced-search" value="Search" class="btn btn-success"> <input type="submit" name="submit-advanced-search" value="Search" class="btn btn-success">
</div> </div>
</form> </form>
<template id="advanced-search-parameter-template">
{% crispy advanced_search_param_formset.empty_form %}
</template>
</div> </div>
<div class="list-group mb-3"> <div class="list-group mb-3">
{% for comp in components %} {% for comp in components %}
@@ -45,7 +48,12 @@
{% endif %} {% endif %}
</div> </div>
<div class="flex-grow-1 ms-3"> <div class="flex-grow-1 ms-3">
<h6 class="mt-0 text-primary">{{ comp.name }}</h6> <h6 class="mt-0 text-primary">
{{ comp.name }}
{% for key_param in comp.get_key_parameters_as_text %}
{{key_param}}
{% endfor %}
</h6>
{% if comp.package %} {% if comp.package %}
Package: {{comp.package}}<br> Package: {{comp.package}}<br>
{% endif %} {% endif %}

View File

@@ -1,22 +0,0 @@
{% load static %}
{% load crispy_forms_tags %}
<div class="modal fade" id="add-sub-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Storage</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="modal-body">
{{form|crispy}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" value="Add Storage" name="submit-add-storage">
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,22 +0,0 @@
{% load static %}
{% load crispy_forms_tags %}
<div class="modal fade" id="change-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Change Storage</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="modal-body">
{{form|crispy}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" value="Change Storage" name="submit-change-storage">
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,46 @@
{% load static %}
<div class="modal fade" id="add-sub-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Storage</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="" method="post">
{% csrf_token %}
<div class="modal-body">
<label for="add-storage-name" class="form-label">Storage Name</label>
<div class="input-group has-validation">
<input value="{{form.storage_name.value}}" class="form-control{% if form.storage_name.errors or form.non_field_errors %} is-invalid{% endif %}" id="add-storage-name" name="{{form.storage_name.name}}" type="text" aria-describedby="validationStorageName" required>
<div id="validationStorageName" class="invalid-feedback">
{% for msg in form.storage_name.errors %}
{{msg}}
{% endfor %}
</div>
</div>
<label for="{{form.responsible.id_for_label}}">Responsible</label>
<div class="input-group has-validation dropdown">
<span class="input-group-text" id="add_storage_username_prepend">@</span><input autocomplete="off" data-bs-toggle="dropdown" type="text" value="{{form.responsible.value}}" class="form-control{% if form.responsible.errors or form.non_field_errors %} is-invalid{% endif %}" id="{{form.responsible.id_for_label}}" name="{{form.responsible.name}}" aria-describedby="add_storage_username_prepend validationServerUsernameFeedback" required>
<ul class="dropdown-menu" aria-labelledby="{{form.responsible.id_for_label}}" id="{{form.responsible.id_for_label}}-ac-dropdown">
</ul>
<div id="validationServerUsernameFeedback" class="invalid-feedback">
{% for msg in form.responsible.errors %}
{{msg}}
{% endfor %}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" value="Add Storage" name="submit-add-storage">
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="text-danger text-center">{{ error }}</p>
{% endfor %}
{% endif %}
</div>
</form>
</div>
</div>
</div>

View File

@@ -27,7 +27,6 @@
{% endif %} {% endif %}
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#delete-storage-modal">Delete</button> <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#delete-storage-modal">Delete</button>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-sub-modal"><i class="bi bi-plus-circle"></i></button> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-sub-modal"><i class="bi bi-plus-circle"></i></button>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#change-modal"><i class="bi bi-pen-fill"></i></button>
</h1> </h1>
<div class="list-group"> <div class="list-group">
{% for storage in storages %} {% for storage in storages %}
@@ -70,7 +69,11 @@
{% endif %} {% endif %}
</div> </div>
<div class="flex-grow-1 ms-3"> <div class="flex-grow-1 ms-3">
<h6 class="mt-0 text-primary"><a href="{% url 'parts-components-detail' uuid=stock.component.id %}" class="text-decoration-none">{{ stock.component.name }}</a></h6> <h6 class="mt-0 text-primary"><a href="{% url 'parts-components-detail' uuid=stock.component.id %}" class="text-decoration-none">{{ stock.component.name }}</a>
{% for key_param in stock.component.get_key_parameters_as_text %}
{{key_param}}&nbsp;
{% endfor %}
</h6>
{% if stock.component.package %} {% if stock.component.package %}
Package: {{stock.component.package}}<br> Package: {{stock.component.package}}<br>
{% endif %} {% endif %}
@@ -110,11 +113,7 @@
{% endfor %} {% endfor %}
<!-- Modal for adding a substorage--> <!-- Modal for adding a substorage-->
{% with add_storage_form as form %} {% with add_storage_form as form %}
{% include 'parts/modals/add-substorage-modal.html' %} {% include 'parts/modals/new-substorage-modal.html' %}
{% endwith %}
<!-- Modal to change current storag-->
{% with change_storage_form as form %}
{% include 'parts/modals/change-storage-modal.html' %}
{% endwith %} {% endwith %}
<!-- Modal for deleting this storage --> <!-- Modal for deleting this storage -->
{% with delete_storage_errors as err_msgs %} {% with delete_storage_errors as err_msgs %}
@@ -163,6 +162,18 @@ api_get_component_from_id(uuid, function(component){
} }
{% endif %} {% endif %}
new AutocompleteText('{{add_storage_form.responsible.id_for_label}}', '{{add_storage_form.responsible.id_for_label}}-ac-dropdown',
function(search, autocomplete_obj) {
api_search_user(search, function(results) {
var usernames = new Array();
console.log(results);
for (var i = 0; i < results.results.length; i++) {
usernames.push(results.results[i].username);
}
console.log(usernames);
autocomplete_obj.show_results(usernames);
}, function(){});
});
</script> </script>
{% endblock custom_scripts %} {% endblock custom_scripts %}

View File

@@ -53,8 +53,8 @@
</div> </div>
<!-- Add storage modal form --> <!-- Add storage modal form -->
{% with add_storage_form as form id as "add-sub-modal"%} {% with add_storage_form as form %}
{% include 'parts/modals/add-substorage-modal.html' %} {% include 'parts/modals/new-substorage-modal.html' %}
{% endwith %} {% endwith %}
</div> </div>
@@ -67,6 +67,18 @@
var modal = bootstrap.Modal.getOrCreateInstance(addSubStorageModal); var modal = bootstrap.Modal.getOrCreateInstance(addSubStorageModal);
modal.show(); modal.show();
{% endif %} {% endif %}
new AutocompleteText('{{add_storage_form.responsible.id_for_label}}', '{{add_storage_form.responsible.id_for_label}}-ac-dropdown',
function(search, autocomplete_obj) {
api_search_user(search, function(results) {
var usernames = new Array();
console.log(results);
for (var i = 0; i < results.results.length; i++) {
usernames.push(results.results[i].username);
}
console.log(usernames);
autocomplete_obj.show_results(usernames);
}, function(){});
});
</script> </script>
{% endblock custom_scripts %} {% endblock custom_scripts %}

View File

@@ -1,12 +1,7 @@
<div class="dropdown"> <div class="dropdown">
<div class="input-group"> <input autocomplete="off" id="{{widget.attrs.id}}" data-ac-url="{{custom.search_url}}" data-ac-name-field="{{custom.name_field_name}}" {% if custom.image_field_name %}data-ac-image-field="{{custom.image_field_name}}"{% endif %} data-bs-toggle="dropdown" type="text" placeholder="Search..." class="{{widget.attrs.class}}">
{% if custom.prepend %} <ul id="{{widget.attrs.id}}-ac-ul" class="dropdown-menu">
<span class="input-group-text" id="{{widget.attrs.id}}-prepend">{{custom.prepend}}</span> </ul>
{% endif %}
<input autocomplete="off" id="{{widget.attrs.id}}" data-ac-url="{{custom.search_url}}" data-ac-name-field="{{custom.name_field_name}}" {% if custom.image_field_name %}data-ac-image-field="{{custom.image_field_name}}"{% endif %} data-bs-toggle="dropdown" type="text" placeholder="Search..." class="{{widget.attrs.class}}">
<ul id="{{widget.attrs.id}}-ac-ul" class="dropdown-menu">
</ul>
</div>
<div class="d-flex align-items-center mt-3 mb-3" id="{{widget.attrs.id}}-dflex-container"> <div class="d-flex align-items-center mt-3 mb-3" id="{{widget.attrs.id}}-dflex-container">
{% if custom.current_instance %} {% if custom.current_instance %}
{% if custom.image_field_name %} {% if custom.image_field_name %}

View File

@@ -1 +0,0 @@
podman run -it -e DJANGO_SECRET_KEY=<secret_key> -e DJANGO_ALLOWED_HOST=parts.shimatta.net -e DJANGO_STATIC_ROOT=/var/static -e DJANGO_MEDIA_URL=media -e DJANGO_MEDIA_ROOT=/var/media -e DJANGO_POSTGRESQL_SOCKET=host.docker.internal -e DJANGO_POSTGRESQL_PORT=2345 -e DJANGO_POSTGRESQL_USER=<db_user> -e DJANGO_POSTGRESQL_PW=<db_pass> -v /var/parts/static:/var/static -v /var/parts/media:/var/media -p 8000:8000 --entrypoint /bin/sh localhost/kenkyusho:0.1