Compare commits

..

20 Commits

Author SHA1 Message Date
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
64 changed files with 1604 additions and 2552 deletions

View File

@ -1,2 +0,0 @@
start_server.sh
run/*

3
.gitignore vendored
View File

@ -125,6 +125,3 @@ dmypy.json
myenv/* myenv/*
run/*
.env

View File

@ -1,6 +0,0 @@
FROM alpine:latest
RUN apk add --no-cache python3 py3-pip python3-dev py3-setuptools gcc python3-dev jpeg-dev zlib-dev musl-dev py3-gunicorn curl
COPY . /home/shimatta/kenkyusho
WORKDIR /home/shimatta/kenkyusho
RUN python3 -m venv /home/shimatta/kenkyusho/.venv && . /home/shimatta/kenkyusho/.venv/bin/activate && pip install -r requirements.txt
ENTRYPOINT ["/home/shimatta/kenkyusho/entrypoint.sh"]

192
README.md
View File

@ -1,192 +0,0 @@
# Shimatta Kenkyusho Parts Database
## Installation
### Prerequisites
Shimatta Kenkyusho (しまった・研究所) is a Django based web application. It is highly recommended to run it using the supplied docker setup. This removes the need of any special installation on the host system. This guide assumes, that `nginx` is running on the host system and can serve as a reverse proxy and webserver. For easiest download, it is recommended to clone the desired release with `git`.
Install the requirements:
**For Debian / Ubuntu:**
```
# apt-get update
# apt-get install docker docker-compose-plugin nginx git
```
**For Arch based Systems:**
```
# pacman -S nginx docker docker-compose git
```
### Setup Shimatta Kenkyusho
Clone this repository:
```
$ git clone https://git.shimatta.de/mhu/shimatta-kenkyusho.git
```
> Note: Shimatta Kenkyusho is currently not stable yet and the newest verison is in the `develop` branch. This will change once actual releases are done and merged to the `master` branch. You will be able to get the latest stable version from the `master` branch or a respective tag. For now, the `develop` is recommended.
Change directory into the `shimatta-kenkyusho` folder cloned by git.
Copy the `example.env` file to `.env` and edit it according to your needs:
The following settings are required to be adapted:
- `DJANGO_STATIC_VOL`: The directory the application will extract its static data into, which needs to be served by your webserver. See the example reverse proxy setup for more details.
- `DJANGO_MEDIA_VOL`: The directory all media files like images uploaded to the application are stored here. This folder must be served by your webserver on the configured media URL.
- `PGDATA_VOL`: The directory, the postgres database will store its files.
- `PORT`: The TCP/IP port that the whole setup will listen on. Use a reverse proxy to forward to this port. *Do not directly expose it to the internet!*
- `DJANGO_SECRET_KEY`: Provide a secret, and randomly generated key. Do not share this with anybody!
- `DJANGO_ALLOWED_HOST`: Set this to the domain, the application will be reached at. E.g: `lab.example.com`
- `DJANGO_MEDIA_URL`: Set this to the media URL at which your webserver serves the `DJANGO_MEDIA_VOL` diretory. E.g: `media.lab.example.com/` Note the **slash at the end**. It is important.
- `DJANGO_USER_ID`: The user ID to run the application inside the docker container. This is the user id, that is used to write the to `DJANGO_STATIC_VOL` and `DJANGO_MEDIA_VOL`. Make sure the user has access.
- `DJANGO_USER_GID`: The group ID to run the application inside the docker container. This is the group id, that is used to write the to `DJANGO_STATIC_VOL` and `DJANGO_MEDIA_VOL`.
> Note: It is not recommended to run the docker container without a set `DJANGO_USER_ID` and `DJANGO_USER_GID`. It will default to `0 (root)`.
Once the environment is set up, the docker containers can be built and started. Run
```
$ docker compose build
```
This will generate two container images:
1. `shimatta-kenkyusho-shimatta-kenkyusho-web`: The django application
2. `postgres`: A alpine based docker container containing the postgres database.
Start the application as a service with
```
$ docker compose up -d
```
> Note: The initial startup might need a minute because the whole database etc. needs to be initialized first.
Use
```
$ docker ps
```
to check if the `shimatta-kenkyusho-shimatta-kenkyusho-db` and the `shimatta-kenkyusho-shimatta-kenkyusho-web` container are running and report a *healthy status*.
### Setup Initial Login User
When started for the first time with a fresh database without any superuser configured, a superuser `admin` with password `admin` will be automatically generated.
Use this user to login for the first time. In the django admin panel you can then either change the password of the `admin` user or create a new superuser with your own username and delete the `admin` user.
As long as there is at least one superuser configured, no admin user will be regenerated upon startup.
### Example Reverse Proxy Setup Using nginx
Once the setup is configured the reverse proxy setup is needed. This setup serves three purposes:
1. Redirect incoming requests to the django application running on the port `PORT` configured in the `.env`
2. Serve static files at the URL: (e.g. `lab.example.com/static`). See `ALLOWED_HOST` configuration.
3. Serve the media volume at the media URL (e.g. `media.lab.example.com`). See `DJANGO_MEDIA_URL`
Example nginx configuration for `nginx >v2.25` with SSL and http2 / http3 support:
> Note: This is by no means a replacement for the documentation of nginx and only serves as an example. Consult the documentation of your nginx version reagrding security and other issues.
```
# Force redirection from http to https for application
server {
listen 80;
listen [::]:80;
server_name lab.example.com; # This must match your ALLOWED_HOST. Adapt domain.
allow all;
return 301 https://lab.example.com$request_uri; # Adapt domain
}
# Force redirection from http to https for media url
server {
listen 80;
listen [::]:80;
server_name media.lab.example.com; # Adapt domain name according to DJANGO_MEDIA_URL
allow all;
return 301 https://media.lab.example.com$request_uri; # Adapt domain name
}
# Reverse Proxy for application
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
# Add this for HTTP3. If your nginx is older than 2.25 this might not be available
######################################################################################
# listen 443 quic reuseport;
# listen [::]:443 quic reuseport;
# Enable QUIC and HTTP/3
# ssl_early_data on;
# add_header Alt-Svc 'h3=":443"; ma=86400';
#######################################################################################
server_name lab.example.com; # Adapt domain
# Use letsencrypt as SSL certificate provider.
ssl_certificate /etc/letsencrypt/live/lab.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/lab.example.com/privkey.pem;
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
allow all;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8000; # Adapt PORT from .env
}
location /static/ {
# Adapt path to static volume here. Note the slash at the end
alias /path/to/DJANGO_STATIC_VOL/;
allow all;
}
client_max_body_size 60m;
}
# Serve the media files
server {
listen 443 ssl;
listen [::]:443 ssl;
# Add this for HTTP3. If your nginx is older than 2.25 this might not be available
######################################################################################
# listen 443 quic reuseport;
# listen [::]:443 quic reuseport;
# Enable QUIC and HTTP/3
# ssl_early_data on;
# add_header Alt-Svc 'h3=":443"; ma=86400';
#######################################################################################
http2 on;
server_name media.lab.example.com; # Adapt according to DJANGO_MEDIA_URL
# Use letsencrypt as SSL certificate provider.
ssl_certificate /etc/letsencrypt/live/media.lab.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/media.lab.example.com/privkey.pem;
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
error_page 502 /lab_down.html;
allow all;
root /path/to /DJANGO_MEDIA_VOL/; # Adapt this to the volume provided.
}
```
Congratulations. Your shimatta kenkyusho installation is now fully setup.
> Note that, the `compose.yaml` contains a restart-policy. By default the containers will restart automatically, even after a reboot of the host machine, if the docker service is enabled.
## Backup and Restore
> TODO
## Debugging and Development
> Todo

View File

@ -1,54 +0,0 @@
x-op-restart-policy: &restart_policy
restart: unless-stopped
services:
shimatta-kenkyusho-web:
<<: *restart_policy
build: .
user: "${DJANGO_USER_ID:-0}:${DJANGO_USER_GID:-0}"
volumes:
- "${DJANGO_STATIC_VOL:-./run/static}:/var/static"
- "${DJANGO_MEDIA_VOL:-./run/media}:/var/media"
environment:
DJANGO_POSTGRESQL_PW: "${DJANGO_POSTGRESQL_PW:-p4ssw0rd}"
DJANGO_POSTGRESQL_USER: "postgres"
DJANGO_SECRET_KEY: "${DJANGO_SECRET_KEY}"
DJANGO_ALLOWED_HOST: "${DJANGO_ALLOWED_HOST}"
DJANGO_STATIC_ROOT: "/var/static"
DJANGO_MEDIA_URL: "${DJANGO_MEDIA_URL}"
DJANGO_MEDIA_ROOT: "/var/media"
DJANGO_POSTGRESQL_SOCKET: "shimatta-kenkyusho-db"
DJANGO_POSTGRESQL_PORT: "5432"
DJANGO_FORCE_DEV_MODE: ${DJANGO_FORCE_DEV_MODE:-False}
ports:
- "${PORT}:8000"
networks:
- backendnet
depends_on:
shimatta-kenkyusho-db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f localhost:8000/healthcheck"]
interval: 5s
timeout: 5s
retries: 5
start_period: 30s
shimatta-kenkyusho-db:
<<: *restart_policy
image: postgres:16.5-alpine
environment:
POSTGRES_PASSWORD: "${DJANGO_POSTGRESQL_PW:-p4ssw0rd}"
POSTGRES_DB: "shimatta_kenkyusho"
volumes:
- "${PGDATA_VOL:-./run/pgdata}:/var/lib/postgresql/data"
networks:
- backendnet
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
networks:
backendnet:

View File

@ -1,8 +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
python manage.py create_kenkyusho_admin_user --settings shimatta_kenkyusho.settings_production
gunicorn -w 4 --bind 0.0.0.0:8000 shimatta_kenkyusho.wsgi:application

View File

@ -1,7 +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 create_kenkyusho_admin_user --settings shimatta_kenkyusho.settings_production
python manage.py runserver 0.0.0.0:8000 --settings shimatta_kenkyusho.settings_production

View File

@ -1,38 +0,0 @@
####################################################################################################
# Example configuration. Must be edited and copied to ".env" next to the compose.yaml
####################################################################################################
# User id to use for the web application. This determines the user id, the media and static files are written to the volumes.
# Make sure the user has rw access to these directories.
DJANGO_USER_ID=1000
# Group id to use for the web application
DJANGO_USER_GID=1000
# Path to to mount as the directory for static data. Must be served by a webserver on the /static path
DJANGO_STATIC_VOL=/path/to/static/root
# Path to the media root. Must be served by a webserver on the media URL
DJANGO_MEDIA_VOL=/path/to/media/root
# folder for DB
PGDATA_VOL=/path/to/pgdata
# Port to serve the App
PORT=8000
# Secret Key. Must be edited before deployment
DJANGO_SECRET_KEY=534hj5jgh4365ghj35jgh245jgh24
# Allowed host to be accessed. Currently it is only possible to specify a single URL
DJANGO_ALLOWED_HOST=lab.example.com
# Media URL for images and other content
DJANGO_MEDIA_URL=media.lab.example.com/
# DO NOT SET DEBUG MODE IN PRODUCTION
# DJANGO_FORCE_DEV_MODE=True
# Set this password if you want to use a custom postgres password. The db should be confined inside the docker network.
# Using the standard PW is therefore not a problem
# DJANGO_POSTGRESQL_PW=myfancynewpassword123donotsharemewithanyone

View File

@ -1,39 +1,25 @@
annotated-types==0.7.0 asgiref==3.4.1
asgiref==3.8.1 astroid==2.6.5
astroid==3.3.5 crispy-bootstrap5==0.6
certifi==2024.8.30 Django==3.2.5
charset-normalizer==3.4.0 django-crispy-forms==1.13.0
crispy-bootstrap5==2024.10 django-filter==2.4.0
dill==0.3.9 django-qr-code==2.2.0
Django==5.1.3
django-crispy-forms==2.3
django-filter==24.3
django-qr-code==4.1.0
django-rest-framework==0.1.0 django-rest-framework==0.1.0
django-tex==1.1.10 django-tex==1.1.9.post1
djangorestframework==3.15.2 djangorestframework==3.12.4
gunicorn==21.2.0
idna==3.10
isort==5.9.3 isort==5.9.3
Jinja2==3.0.1 Jinja2==3.0.1
lazy-object-proxy==1.6.0 lazy-object-proxy==1.6.0
MarkupSafe==2.0.1 MarkupSafe==2.0.1
mccabe==0.6.1 mccabe==0.6.1
packaging==24.2
Pillow==8.3.1 Pillow==8.3.1
pipdeptree==2.23.4 psycopg2==2.9.1
platformdirs==4.3.6 pylint==2.9.6
psycopg2-binary==2.9.9 pytz==2021.1
pydantic==2.9.2 qrcode==7.2
pydantic_core==2.23.4 segno==1.3.3
pylint==3.3.1 six==1.16.0
requests==2.32.3
segno==1.6.1
setuptools==75.3.0
sqlparse==0.4.1 sqlparse==0.4.1
toml==0.10.2 toml==0.10.2
tomlkit==0.13.2
typing_extensions==4.12.2
tzdata==2024.2
urllib3==2.2.3
wrapt==1.12.1 wrapt==1.12.1

View File

@ -6,9 +6,10 @@
"configurations": [ "configurations": [
{ {
"name": "Python: Django", "name": "Python: Django",
"type": "debugpy", "type": "python",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/shimatta_kenkyusho/manage.py", "pythonPath": "${workspaceFolder}/../myenv/bin/python",
"program": "${workspaceFolder}/manage.py",
"args": [ "args": [
"runserver", "runserver",
"0.0.0.0:8000" "0.0.0.0:8000"

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:
@ -24,72 +23,32 @@ class PackageSerializer(serializers.HyperlinkedModelSerializer):
model = parts_models.Package model = parts_models.Package
fields = '__all__' fields = '__all__'
class ComponentParameterSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.ReadOnlyField()
ro_parameter_type = serializers.ReadOnlyField(source='parameter_type.parameter_name')
class Meta:
model = parts_models.ComponentParameter
fields = '__all__'
class ComponentDistributorNumSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.ReadOnlyField()
class Meta:
model = parts_models.DistributorNum
fields = '__all__'
class ComponentSerializer(serializers.HyperlinkedModelSerializer):
package_data = PackageSerializerNoLink(source='package', read_only=True)
ro_manufacturer_name = serializers.ReadOnlyField(source='manufacturer.name')
ro_image = serializers.ReadOnlyField(source='get_resolved_image')
ro_component_type = serializers.ReadOnlyField(source='component_type.class_name')
ro_parameters = ComponentParameterSerializer(many=True, source='componentparameter_set', read_only=True)
ro_distributor_numbers = ComponentDistributorNumSerializer(many=True, source='distributornum_set', read_only=True)
ro_dynamic_description = serializers.ReadOnlyField(source='dynamic_description')
class Meta:
model = parts_models.Component
fields = ['url',
'id',
'name',
'package_data',
'package',
'pref_distri',
'image',
'manufacturer',
'component_type',
'ro_manufacturer_name',
'ro_image',
'ro_component_type',
'ro_parameters',
'ro_distributor_numbers',
'ro_dynamic_description']
class StockSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.ReadOnlyField()
ro_package_name = serializers.ReadOnlyField(source='component.package.name')
ro_component_name = serializers.ReadOnlyField(source='component.name')
ro_component_dynamic_description = serializers.ReadOnlyField(source='component.dynamic_description')
ro_manufacturer_name = serializers.ReadOnlyField(source='component.manufacturer.name')
ro_image = serializers.ReadOnlyField(source='component.get_resolved_image')
class Meta:
model = parts_models.Stock
fields = '__all__'
class StockSerializerExpandComponent(StockSerializer):
ro_component = ComponentSerializer(read_only=True, source='component')
class StorageSerializer(serializers.HyperlinkedModelSerializer): class StorageSerializer(serializers.HyperlinkedModelSerializer):
full_path = serializers.ReadOnlyField(source='get_full_path') full_path = serializers.ReadOnlyField(source='get_full_path')
class Meta: class Meta:
model = parts_models.Storage model = parts_models.Storage
fields = ['url', 'id', 'name', 'verbose_name', 'parent_storage', 'responsible', 'template', 'full_path', 'full_path_verbose'] fields = ['url', 'id', 'name', 'parent_storage', 'responsible', 'full_path']
class StorageSerializerStocksExpanded(StorageSerializer): class ComponentSerializer(serializers.HyperlinkedModelSerializer):
ro_stocks = StockSerializerExpandComponent(many=True, read_only=True, source='stock_set') package_data = PackageSerializerNoLink(source='package', read_only=True)
ro_manufacturer_name = serializers.ReadOnlyField(source='manufacturer.name')
ro_image = serializers.ReadOnlyField(source='get_resolved_image')
class Meta(StorageSerializer.Meta): class Meta:
fields = StorageSerializer.Meta.fields + ['ro_stocks'] model = parts_models.Component
fields = ['url', 'id', 'name', 'package_data', 'package', 'pref_distri', 'image', 'manufacturer', 'ro_manufacturer_name', 'ro_image']
class StockSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.ReadOnlyField()
ro_package_name = serializers.ReadOnlyField(source='component.package.name')
ro_component_name = serializers.ReadOnlyField(source='component.name')
ro_manufacturer_name = serializers.ReadOnlyField(source='component.manufacturer.name')
ro_image = serializers.ReadOnlyField(source='component.get_resolved_image')
class Meta:
model = parts_models.Stock
fields = '__all__'
class DistributorSerializer(serializers.HyperlinkedModelSerializer): class DistributorSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.ReadOnlyField() id = serializers.ReadOnlyField()

View File

@ -1,15 +1,13 @@
from django.urls import include, path, re_path from django.urls import include, 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/component-parameters', PartsComponentParameterViewSet)
router.register(r'parts/component-distributor-numbers', PartsComponentDistributorNumViewSet)
router.register(r'parts/stocks', PartsStockViewSet) router.register(r'parts/stocks', PartsStockViewSet)
router.register(r'parts/packages', PartsPackageViewSet) router.register(r'parts/packages', PartsPackageViewSet)
router.register(r'parts/distributors', PartsDistributorviewSet) router.register(r'parts/distributors', PartsDistributorviewSet)
@ -19,6 +17,6 @@ router.register(r'parts/component-param-types', PartsComponentParameterTypeViewS
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
re_path(r'^token-auth/', ObtainExpiringAuthToken.as_view()), url(r'^token-auth/', ObtainExpiringAuthToken.as_view()),
re_path(r'^token-logout/', TokenLogout.as_view()), url(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]
@ -42,23 +41,10 @@ class GroupViewSet(viewsets.ReadOnlyModelViewSet):
class PartsStorageViewSet(viewsets.ModelViewSet): class PartsStorageViewSet(viewsets.ModelViewSet):
queryset = parts_models.Storage.objects.all() queryset = parts_models.Storage.objects.all()
permission_classes = [permissions.DjangoModelPermissions]
filter_backends = [django_filters.rest_framework.DjangoFilterBackend, filters.SearchFilter]
search_fields = ['id', 'name', 'verbose_name']
filterset_fields = ['id', 'name', 'parent_storage']
def get_serializer_class(self):
if self.request.GET.get('expand_stocks'):
return StorageSerializerStocksExpanded
return StorageSerializer
class PartsStorageTemplatesViewSet(viewsets.ReadOnlyModelViewSet):
queryset = parts_models.Storage.objects.filter(is_template=True)
serializer_class = StorageSerializer serializer_class = StorageSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.DjangoModelPermissions]
filter_backends = [filters.SearchFilter] filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
search_fields = ['id', 'name'] filterset_fields = ['id', 'name', 'parent_storage']
filterset_fields = ['id', 'name']
class PartsComponentViewSet(viewsets.ModelViewSet): class PartsComponentViewSet(viewsets.ModelViewSet):
queryset = parts_models.Component.objects.all() queryset = parts_models.Component.objects.all()
@ -68,22 +54,6 @@ class PartsComponentViewSet(viewsets.ModelViewSet):
search_fields = ['id', 'name', 'package__name', 'manufacturer__name'] search_fields = ['id', 'name', 'package__name', 'manufacturer__name']
filterset_fields = ['id', 'name'] filterset_fields = ['id', 'name']
class PartsComponentParameterViewSet(viewsets.ModelViewSet):
queryset = parts_models.ComponentParameter.objects.all()
serializer_class = ComponentParameterSerializer
permission_classes = [permissions.DjangoModelPermissions]
filter_backends = [filters.SearchFilter, django_filters.rest_framework.DjangoFilterBackend]
search_fields = ['id', 'parameter_type__parameter_name']
filterset_fields = ['id']
class PartsComponentDistributorNumViewSet(viewsets.ModelViewSet):
queryset = parts_models.DistributorNum.objects.all()
serializer_class = ComponentDistributorNumSerializer
permission_classes = [permissions.DjangoModelPermissions]
filter_backends = [filters.SearchFilter, django_filters.rest_framework.DjangoFilterBackend]
search_fields = ['id', 'distributor', 'distributor_part_number']
filterset_fields = ['id']
class PartsComponentTypeViewSet(viewsets.ModelViewSet): class PartsComponentTypeViewSet(viewsets.ModelViewSet):
queryset = parts_models.ComponentType.objects.all() queryset = parts_models.ComponentType.objects.all()
serializer_class = ComponentTypeSerializer serializer_class = ComponentTypeSerializer

View File

@ -1,27 +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 .qr_parser import QrCodeValidator
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)
@ -51,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
@ -89,32 +74,21 @@ 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="Name", initial='') storage_name = forms.CharField(label="storage_name", initial='')
verbose_name = forms.CharField(label="Verbose Name", initial='', required=False) responsible = forms.CharField(label='responsible_user')
responsible = AutocompleteForeingKeyField(api_search_url='user-list',
image_field_name=None,
name_field_name='username',
foreign_model=get_user_model(),
prepend='@')
expand_sub_storage_stocks = forms.BooleanField(label='Expand sub storage Stocks', required=False)
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()
class EditStockBaseForm(forms.Form): class EditWatermarkForm(forms.Form):
stock_uuid = forms.UUIDField() stock_uuid = forms.UUIDField()
watermark_active = forms.BooleanField(required=False) #If it is false, the webbrowser won't send it at all. Therefore we have to set it to required=False
watermark = forms.IntegerField(min_value=0)
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
@ -132,10 +106,6 @@ class EditStockBaseForm(forms.Form):
return cleaned_data return cleaned_data
class EditWatermarkForm(EditStockBaseForm):
watermark_active = forms.BooleanField(required=False) #If it is false, the webbrowser won't send it at all. Therefore we have to set it to required=False
watermark = forms.IntegerField(min_value=0)
def save(self): def save(self):
stock = self.cleaned_data['stock'] stock = self.cleaned_data['stock']
active = self.cleaned_data['watermark_active'] active = self.cleaned_data['watermark_active']
@ -147,30 +117,26 @@ class EditWatermarkForm(EditStockBaseForm):
stock.watermark = watermark stock.watermark = watermark
stock.save() stock.save()
class EditLotForm(EditStockBaseForm): class EditStockAmountForm(forms.Form):
lot = forms.IntegerField(min_value=0) stock_uuid = forms.UUIDField()
def save(self):
stock = self.cleaned_data['stock']
lot = self.cleaned_data['lot']
stock.lot = lot
stock.save()
class RelocateStockForm(forms.ModelForm):
storage = AutocompleteForeingKeyField(api_search_url='storage-list',
foreign_model=parts_models.Storage,
name_field_name='full_path_verbose',
image_field_name=None,
required=True)
class Meta:
model = parts_models.Stock
fields = ['storage']
class EditStockAmountForm(EditStockBaseForm):
amount = forms.IntegerField(min_value=0) amount = forms.IntegerField(min_value=0)
def clean(self):
cleaned_data = super().clean()
id = cleaned_data.get("stock_uuid")
if not id:
raise ValidationError("No stock UUID given")
stock = None
try:
stock = parts_models.Stock.objects.get(id=id)
except:
raise ValidationError("Stock with uuid %s does not exist" % (id))
cleaned_data['stock'] = stock
return cleaned_data
def save(self, increase: bool): def save(self, increase: bool):
stock = self.cleaned_data['stock'] stock = self.cleaned_data['stock']
amount = self.cleaned_data['amount'] amount = self.cleaned_data['amount']
@ -222,9 +188,6 @@ class ComponentForm(forms.ModelForm):
model = parts_models.Component model = parts_models.Component
fields = '__all__' fields = '__all__'
class ImportComponentForm(forms.Form):
csv_file = forms.FileField(label="CSV File")
class PackageForm(forms.ModelForm): class PackageForm(forms.ModelForm):
class Meta: class Meta:
model = parts_models.Package model = parts_models.Package
@ -259,19 +222,15 @@ class DistributorNumberDeleteForm(forms.Form):
class ComponentParameterDeleteForm(forms.Form): class ComponentParameterDeleteForm(forms.Form):
param_num = forms.UUIDField(required=True) param_num = forms.UUIDField(required=True)
model = parts_models.ComponentParameter
def clean_param_num(self): def clean_param_num(self):
my_uuid = self.cleaned_data['param_num'] my_uuid = self.cleaned_data['param_num']
try: try:
param = self.model.objects.get(id=my_uuid) param = parts_models.ComponentParameter.objects.get(id=my_uuid)
except: except:
raise ValidationError('Parameter Number Invalid') raise ValidationError('Parameter Number Invalid')
return param return param
class PackageParameterDeleteForm(ComponentParameterDeleteForm):
model = parts_models.PackageParameter
class AdvancedComponentSearchForm(forms.Form): class AdvancedComponentSearchForm(forms.Form):
name = forms.CharField(max_length=255, label='Component Name', required=False) name = forms.CharField(max_length=255, label='Component Name', required=False)
package = AutocompleteForeingKeyField(required=False, api_search_url='package-list', foreign_model=parts_models.Package) package = AutocompleteForeingKeyField(required=False, api_search_url='package-list', foreign_model=parts_models.Package)
@ -285,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'),
@ -303,24 +263,56 @@ 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)
model = parts_models.ComponentParameter
def clean(self): def clean(self):
data = super().clean() data = super().clean()
@ -350,26 +342,4 @@ class ComponentParameterCreateForm(forms.Form):
else: else:
text_value = '' text_value = ''
value = self.cleaned_data['number_value'] value = self.cleaned_data['number_value']
self.model.objects.create(parameter_type=param_type, component=component, value=value, text_value=text_value) parts_models.ComponentParameter.objects.create(parameter_type=param_type, component=component, value=value, text_value=text_value)
class PackageParameterCreateForm(ComponentParameterCreateForm):
model = parts_models.PackageParameter
def save(self, package):
param_type = self.cleaned_data['parameter_type']
if param_type.parameter_type == 'F':
text_value = self.cleaned_data['value']
value = 0
else:
text_value = ''
value = self.cleaned_data['number_value']
self.model.objects.create(parameter_type=param_type, package=package, value=value, text_value=text_value)
class QrSearchForm(forms.Form):
my_qr_validator = QrCodeValidator()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
qr_search = forms.CharField(label='qr_search', validators=[my_qr_validator])

View File

@ -1,43 +0,0 @@
from django.core.management.base import BaseCommand, CommandParser
from django.contrib.auth import get_user_model
from parts.models import Component, ComponentParameter, ComponentParameterType, PackageParameter, Package
class Command(BaseCommand):
help = "Remove component parameters, that are also set on the package with the same value"
def add_arguments(self, parser: CommandParser):
parser.add_argument('--dry-run',
help='Do not perform parameter deletion. Print only',
action='store_true')
def handle(self, *args, **options):
# Get all components with set packages. Ignore the ones without packages
all_comps = Component.objects.exclude(package__isnull=True)
for component in all_comps:
package_parameters = PackageParameter.objects.filter(package=component.package)
component_parameters = ComponentParameter.objects.filter(component=component)
package_param_ids = package_parameters.values_list('parameter_type_id', flat=True)
component_param_ids = component_parameters.values_list('parameter_type_id', flat=True)
# Skip trivial cases
if len(package_param_ids) == 0 or len(component_param_ids) == 0:
continue
self.stdout.write(f'Comp: {str(component)} Found {len(component_param_ids)} different parameters')
self.stdout.write(f'\tPackage: {str(component.package)} Found {len(package_param_ids)} different parameters')
commontypes = ctypes = ComponentParameterType.objects.filter(id__in=component_param_ids).filter(id__in=package_param_ids)
self.stdout.write(f'\tCommon parameter count: {len(commontypes)}')
# Check if values are the same when rendered as a string. This avoids float comparison problems
for common_type in commontypes:
s1 = package_parameters.filter(parameter_type=common_type).first().resolved_value_as_string()
comp_param = component_parameters.filter(parameter_type=common_type).first()
s2 = comp_param.resolved_value_as_string()
if s1 == s2:
self.stdout.write(f'\tParameter {common_type.parameter_name} is the same value for component and package: {s1}. Removing from component')
if not options['dry_run']:
comp_param.delete()
else:
self.stderr.write(f'\tParameter {common_type.parameter_name} is set on component {str(component)} and its package with different values: "{s1}" vs "{s2}"')

View File

@ -1,23 +0,0 @@
from django.core.management.base import BaseCommand, CommandParser
from django.contrib.auth import get_user_model
class Command(BaseCommand):
help = "Create a default superuser if no superuser is already present. This aids automatic deployment inside a container."
def add_arguments(self, parser: CommandParser):
parser.add_argument('--user',
help='Username to create if no admin account is present',
default='admin')
parser.add_argument('--password',
help='Password to set for newly created user. Ignored, if any admin user is already present',
default='admin')
def handle(self, *args, **options):
User = get_user_model()
# Query if there is any admin user
if not User.objects.filter(is_superuser=True).exists():
self.stdout.write(f'No superuser present. Creating {options['user']} with supplied password')
User.objects.create_superuser(username=options['user'], password=options['password'])
else:
self.stdout.write('At least one superuser already exists. Skipping superuser creation')

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,18 +0,0 @@
# Generated by Django 3.2.5 on 2024-11-16 11:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parts', '0011_auto_20241110_1242'),
]
operations = [
migrations.AddField(
model_name='storage',
name='verbose_name',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@ -1,49 +0,0 @@
# Generated by Django 5.1.3 on 2024-11-19 20:26
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("parts", "0012_storage_verbose_name"),
]
operations = [
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,18 +0,0 @@
# Generated by Django 5.1.3 on 2025-01-25 14:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parts', '0013_packageparameter'),
]
operations = [
migrations.AddField(
model_name='storage',
name='expand_sub_storage_stocks',
field=models.BooleanField(default=False),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.3 on 2025-01-31 21:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parts', '0014_storage_expand_sub_storage_stocks'),
]
operations = [
migrations.AddField(
model_name='componenttype',
name='description_template',
field=models.TextField(blank=True),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.3 on 2025-01-31 21:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parts', '0015_componenttype_description_template'),
]
operations = [
migrations.AddField(
model_name='componentparametertype',
name='interfix',
field=models.CharField(blank=True, max_length=10),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 5.1.3 on 2025-02-02 10:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parts', '0016_componentparametertype_interfix'),
]
operations = [
migrations.AlterField(
model_name='componentparametertype',
name='interfix',
field=models.CharField(blank=True, help_text='char to be used as decimal point in dynamic description eg. 2R2', max_length=10),
),
migrations.AlterField(
model_name='componenttype',
name='description_template',
field=models.TextField(blank=True, help_text="Template to assemble the dynamic description. Use template syntax, access the component with 'object', parameters with 'param_*'."),
),
]

View File

@ -1,12 +1,11 @@
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
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.template import engines
import os import os
import uuid import uuid
from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter as NumConv from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter as NumConv
@ -30,7 +29,6 @@ class ComponentParameterType(models.Model):
parameter_name = models.CharField(max_length=50, unique=True) parameter_name = models.CharField(max_length=50, unique=True)
parameter_description = models.TextField(null=False, blank=True) parameter_description = models.TextField(null=False, blank=True)
unit = models.CharField(max_length=10, null=False, blank=True) unit = models.CharField(max_length=10, null=False, blank=True)
interfix = models.CharField(max_length=10, null=False, blank=True, help_text="char to be used as decimal point in dynamic description eg. 2R2")
parameter_type = models.CharField(max_length=1, choices=TYPE_CHOICES, default='N') parameter_type = models.CharField(max_length=1, choices=TYPE_CHOICES, default='N')
def __str__(self): def __str__(self):
@ -49,14 +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)
description_template = models.TextField(blank=True, key_parameter1 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param1")
help_text="Template to assemble the dynamic description. " key_parameter2 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param2")
"Use template syntax, access the component with 'object', parameters with 'param_*'.") 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:
@ -67,53 +86,28 @@ 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])
verbose_name = models.CharField(max_length=100, null=True, blank=True)
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)
expand_sub_storage_stocks = models.BooleanField(default=False)
# allow storages to be templates which can be selected when adding new storages def get_path_components(self):
is_template = models.BooleanField(default=False)
template = models.ForeignKey('self',
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name='template_of')
# caching variable for subtrees
storage_list = None
def get_path_components(self, top_level=None):
chain = [] chain = []
iterator = self iterator = self
chain.append(self) chain.append(self)
while iterator.parent_storage is not None: while iterator.parent_storage is not None:
if top_level and iterator.parent_storage == top_level:
break
chain.append(iterator.parent_storage) chain.append(iterator.parent_storage)
iterator = iterator.parent_storage iterator = iterator.parent_storage
return chain return chain
def get_full_path(self, top_level=None): def get_full_path(self):
output = '' output = ''
chain = self.get_path_components(top_level) chain = self.get_path_components()
for i in range(len(chain) - 1, -1, -1): for i in range(len(chain) - 1, -1, -1):
output = output + '/' + chain[i].name output = output + '/' + chain[i].name
return output return output
@property
def full_path_verbose(self):
full_path = f'{self.get_full_path()} ({self.id})'
if self.verbose_name:
full_path += f' ({self.verbose_name})'
return full_path
def get_qr_code(self): def get_qr_code(self):
qrdata = '[stor_uuid]' + str(self.id) qrdata = '[stor_uuid]' + str(self.id)
return qrdata return qrdata
@ -127,19 +121,6 @@ class Storage(models.Model):
else: else:
return self.storage_set.all() return self.storage_set.all()
@classmethod
def get_substorage_list(cls, sub_storages):
sub_sub_storages = cls.objects.filter(parent_storage__in=sub_storages)
final_sub_storages = sub_storages | sub_sub_storages
if sub_sub_storages:
final_sub_storages |= cls.get_substorage_list(sub_sub_storages)
return final_sub_storages
def get_storage_list(self):
return Storage.objects.filter(id=self.id) | self.get_substorage_list(self.storage_set.all())
def validate_unique(self, exclude=None): def validate_unique(self, exclude=None):
if Storage.objects.exclude(id=self.id).filter(name=self.name, parent_storage__isnull=True).exists(): if Storage.objects.exclude(id=self.id).filter(name=self.name, parent_storage__isnull=True).exists():
if self.parent_storage is None: if self.parent_storage is None:
@ -150,41 +131,12 @@ class Storage(models.Model):
raise raise
def get_total_stock_amount(self): def get_total_stock_amount(self):
stocks = Stock.objects.filter(storage__in=self.storage_list or self.get_storage_list()) stocks = Stock.objects.filter(storage=self)
sum = stocks.aggregate(Sum('amount'))['amount__sum'] sum = stocks.aggregate(Sum('amount'))['amount__sum']
if sum is None: if sum is None:
sum = 0 sum = 0
return sum return sum
def get_total_stock_count(self):
return Stock.objects.filter(storage__in=self.storage_list or self.get_storage_list()).count()
def get_total_substorage_amount(self):
return len(self.storage_list or self.get_storage_list()) - 1 # -1 as thhe storage list counts the parent storage as well
@classmethod
def from_path(cls, path, root_storage=None):
'''
Get the storage object described by its complete path or the sub-path
from the passed root_storage uuid
'''
parts = path.split('/')
# assemble filter query
filter_dict = {}
layer = 0
for part in parts[::-1]:
filter_dict[f'{"parent_storage__" * layer}name'] = part
layer += 1
if root_storage:
filter_dict[f'{"parent_storage__" * layer}id'] = root_storage
else:
filter_dict[f'{"parent_storage__" * layer}isnull'] = True
obj = cls.objects.get(**filter_dict)
return obj
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.validate_unique() self.validate_unique()
super(Storage, self).save(*args, **kwargs) super(Storage, self).save(*args, **kwargs)
@ -216,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:
@ -278,41 +261,51 @@ class Component(models.Model):
sum = 0 sum = 0
return sum return sum
@property def get_key_parameters(self):
def dynamic_description(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 not self.component_type or not self.component_type.description_template: if self.component_type:
return '' 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()
django_engine = engines["django"] return (p1, p2, p3)
template = django_engine.from_string(self.component_type.description_template)
parameters = list(ComponentParameter.objects.filter(component=self)) def get_key_parameters_as_text(self):
parameters += list(PackageParameter.objects.filter(package=self.package)) params = self.get_key_parameters()
ret_strings = []
for p in params:
if p:
ret_strings.append(p.resolved_value_as_string())
return ret_strings
context = {f'param_{param.parameter_type.parameter_name}': param for param in parameters} class ComponentParameter(models.Model):
context.update({'object': self})
return template.render(context)
class AbstractParameter(models.Model):
class Meta: class Meta:
abstract = True unique_together = ('component', 'parameter_type')
ordering = ['id']
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)
component = models.ForeignKey(Component, on_delete=models.CASCADE) # A target component is required!
parameter_type = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE) parameter_type = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE)
value = models.FloatField(default=0) value = models.FloatField(default=0)
text_value = models.TextField(null=False, blank=True) text_value = models.TextField(null=False, blank=True)
def _get_object_of_param(self):
return None
def __str__(self): def __str__(self):
if self.parameter_type.parameter_type == 'F': if self.parameter_type.parameter_type == 'F':
value = self.text_value value = self.text_value
else: else:
value = str(self.value) value = str(self.value)
return str(self._get_object_of_param())+ ': '+ str(self.parameter_type) + ': ' + value return str(self.component)+ ': '+ str(self.parameter_type) + ': ' + value
def resolved_value_as_string(self): def resolved_value_as_string(self):
my_type = self.parameter_type.parameter_type my_type = self.parameter_type.parameter_type
@ -320,50 +313,15 @@ class AbstractParameter(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
def resolved_value_as_short_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))
result = f'{round(num, 3):g}'
interpostfix = (prefix if prefix else self.parameter_type.interfix or '.')
if '.' in result:
result = result.replace('.', interpostfix)
else:
result = result + interpostfix
return result
elif my_type == 'N':
# Standard float number
return f'{round(self.value, 3):g}{self.parameter_type.unit}'
else:
return self.resolved_value_as_string()
class ComponentParameter(AbstractParameter):
class Meta:
unique_together = ('component', 'parameter_type')
ordering = ['id']
component = models.ForeignKey(Component, on_delete=models.CASCADE) # A target component is required!
def _get_object_of_param(self):
return self.component
class PackageParameter(AbstractParameter):
class Meta:
unique_together = ('package', 'parameter_type')
ordering = ['id']
package = models.ForeignKey(Package, on_delete=models.CASCADE)
def _get_object_of_param(self):
return self.package
class Stock(models.Model): class Stock(models.Model):
class Meta: class Meta:
unique_together = ('component', 'storage') unique_together = ('component', 'storage')
@ -383,7 +341,7 @@ class Stock(models.Model):
return True return True
def get_qr_code(self): def get_qr_code(self):
qr_data = '[stck_uuid]'+str(self.id) qr_data = '[stock]'+str(self.id)
return qr_data return qr_data
def __str__(self): def __str__(self):
@ -414,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)])
@ -462,26 +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.
"""
# Skip recursion if the model is saved 'raw' e.g. when imported
if 'raw' in kwargs:
if kwargs['raw']:
return
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,
expand_sub_storage_stocks=instance.expand_sub_storage_stocks,
is_template=False,
template=sub_storage)

View File

@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.urls import reverse as url_reverse from django.urls import reverse as url_reverse
import re import re
from .models import Storage, Component, Stock from .models import Storage, Component
class QrCode: class QrCode:
prefix = '' prefix = ''
@ -19,7 +19,6 @@ class QrCodeValidator:
qr_patterns = { qr_patterns = {
'stor_uuid': QrCode('stor_uuid', 'parts-stocks-detail', Storage), 'stor_uuid': QrCode('stor_uuid', 'parts-stocks-detail', Storage),
'comp_uuid': QrCode('comp_uuid', 'parts-components-detail', Component), 'comp_uuid': QrCode('comp_uuid', 'parts-components-detail', Component),
'stck_uuid': QrCode('stck_uuid', 'parts-stock-detail', Stock),
} }
def __init__(self): def __init__(self):
@ -33,13 +32,16 @@ class QrCodeValidator:
qr_type = matches.group('prefix') qr_type = matches.group('prefix')
qr_uuid = matches.group('uuid') qr_uuid = matches.group('uuid')
try:
url_name = self.qr_patterns[qr_type].detail_view url_name = self.qr_patterns[qr_type].detail_view
model = None
try:
model = self.qr_patterns[qr_type].model model = self.qr_patterns[qr_type].model
except KeyError as ex: except:
raise ValidationError('QR Pattern not registered') from ex model = None
return (model, qr_uuid, url_name) if model is None:
raise ValidationError('QR Pattern not registered')
return (model,qr_uuid, url_name)
def get_redirect_url(self, data): def get_redirect_url(self, data):
model, uuid, url_name = self._get_model_from_qr(data) model, uuid, url_name = self._get_model_from_qr(data)

View File

@ -1,9 +0,0 @@
import datetime
from django import template
register = template.Library()
@register.filter(name="get_relative_storage_path")
def get_relative_storage_path(storage, top_level):
return f'.{storage.get_full_path(top_level)}'

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'),
@ -14,7 +15,7 @@ urlpatterns = [
path('components/<slug:uuid>/', parts_views.ComponentDetailView.as_view(), name='parts-components-detail'), path('components/<slug:uuid>/', parts_views.ComponentDetailView.as_view(), name='parts-components-detail'),
path('packages/<slug:uuid>/', parts_views.PackageDetailView.as_view(), name='parts-packages-detail'), path('packages/<slug:uuid>/', parts_views.PackageDetailView.as_view(), name='parts-packages-detail'),
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.ManufacturersView.as_view(), name='parts-manufacturers'), path('manufacturers/', parts_views.ManufacturersViewSet.as_view(), name='parts-manufacturers'),
path("manufacturers/<slug:uuid>/", parts_views.ManufacturerDetailView.as_view(), name='parts-manufacturers-detail'), path("manufacturers/<slug:uuid>/", parts_views.ManufacturerDetailViewSet.as_view(), name='parts-manufacturers-detail'),
path("healthcheck/", parts_views.health_check_view, name='parts-health-check'), path("componenttypes/<slug:uuid>/", parts_views.ComponentTypeDetailView.as_view(), name='parts-componenttypes-detail'),
] ]

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
from .component_views import *
from .distributor_views import *
from .generic_views import *
from .manufacturer_views import *
from .package_views import *
from .storage_views import *

View File

@ -1,110 +0,0 @@
import io
import csv
import requests
from django.db import transaction
from django.core.files.images import ImageFile
from ..models import ComponentParameter, ComponentType, Manufacturer, Component, \
DistributorNum, Stock, Storage, Distributor, Package, ComponentParameterType
def _stock_component(component, storage_uuid, substorage_path, amount, lot, watermark):
if not amount or not any([storage_uuid, substorage_path]):
return None
storage = Storage.from_path(substorage_path, storage_uuid)
stock = Stock.objects.create(component=component,
storage=storage,
amount=amount,
lot=lot,
watermark=watermark)
return stock
def _set_additional_parameters(component, type, value):
if type.startswith('param:'):
type = type[6:]
param_type = ComponentParameterType.objects.get(parameter_name=type)
param = ComponentParameter.objects.create(component=component,
parameter_type=param_type)
if param_type.parameter_type == 'F':
param.text_value = value
else:
param.value = float(value)
param.save()
return param
elif type.startswith('distri:'):
distri_name = type[7:]
distri = Distributor.objects.get(name=distri_name)
distri_num = DistributorNum.objects.create(component=component,
distributor=distri,
distributor_part_number=value)
return distri_num
def import_components_from_csv(csv_file):
"""
Imports components from a csv file containing the component model fields as
well as storage information in the heading.
Parameters can be set by param:<parameter name>, distri numbers by
distri:<distri name>.
"""
with transaction.atomic():
# simulate a text-file for the csv lib
with io.TextIOWrapper(csv_file, encoding='utf8') as csv_text_file:
rows = csv.DictReader(csv_text_file, delimiter=";")
for data in rows:
image = None
image_url = data.pop('image_url')
if image_url:
response = requests.get(image_url)
image_content = response.content
image_file = io.BytesIO(image_content)
image = ImageFile(image_file, 'downloaded_file')
manufacturer = None
manufacturer_name = data.pop('manufacturer')
if manufacturer_name:
manufacturer = Manufacturer.objects.get(name=manufacturer_name)
component_type = None
component_type_name = data.pop('component_type')
if component_type_name:
component_type = ComponentType.objects.get(class_name=component_type_name)
distributor = None
pref_distri = data.pop('pref_distri')
if pref_distri:
distributor = Distributor.objects.get(name=pref_distri)
package = None
package_name = data.pop('package')
if package_name:
package = Package.objects.get(name=package_name)
comp = Component.objects.create(name=data.pop('name'),
manufacturer=manufacturer,
component_type=component_type,
pref_distri=distributor,
description=data.pop('description'),
datasheet_link=data.pop('datasheet_link'),
package=package,
image=image)
_stock_component(comp,
data.pop('storage_uuid'),
data.pop('substorage_path'),
data.pop('amount'),
data.pop('lot'),
data.pop('watermark'))
for key, value in data.items():
_set_additional_parameters(comp, key, value)

View File

@ -1,274 +0,0 @@
import uuid
from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic import TemplateView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import Paginator
from django.db.models import Q
from django.forms import formset_factory
from django.db import IntegrityError
from django.db.models import ProtectedError
from ..models import Stock, Component, ComponentParameter, DistributorNum, PackageParameter
from ..forms import *
from .component_import import import_components_from_csv
from .generic_views import BaseTemplateMixin
ParameterSearchFormSet = formset_factory(ComponentParameterSearchForm, extra=1)
# Create your views here.
class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/components.html'
base_title = 'Components'
navbar_selected = 'Components'
default_page_size = 25
def get_component_query_set(self, search_string):
queryset = Component.objects.all()
if not search_string:
return queryset
search_fragments = search_string.strip().split()
for search in search_fragments:
queryset = queryset.filter(Q(name__icontains = search) | Q(manufacturer__name__icontains = search) | Q(package__name__icontains = search))
return queryset
def get_component_queryset_from_advanced_search(self, cleaned_data):
queryset = Component.objects.all()
if cleaned_data['name']:
queryset = queryset.filter(Q(name__icontains=cleaned_data['name']))
if cleaned_data['package']:
queryset = queryset.filter(package=cleaned_data['package'])
if cleaned_data['package_pin_count']:
queryset = queryset.filter(package__pin_count=cleaned_data['package_pin_count'])
if cleaned_data['component_type']:
queryset = queryset.filter(component_type=cleaned_data['component_type'])
if cleaned_data['distributor_num']:
if cleaned_data['distributor']:
distri = cleaned_data['distributor']
queryset = queryset.filter(Q(distributornum__distributor_part_number__icontains=cleaned_data['distributor_num']) & Q(distributornum__distributor=distri))
else:
queryset = queryset.filter(Q(distributornum__distributor_part_number__icontains=cleaned_data['distributor_num']))
if cleaned_data['manufacturer']:
queryset = queryset.filter(manufacturer=cleaned_data['manufacturer'])
return queryset
def get_context_data_int(self, advanced_search, parameter_formset : ParameterSearchFormSet, **kwargs):
context = super().get_context_data(**kwargs)
comp_page_num = self.request.GET.get('comp_page', default=1)
if advanced_search and parameter_formset:
search = None
context['advanced_search_shown'] = True
context['advanced_search_form'] = advanced_search
context['advanced_search_param_formset'] = parameter_formset
if advanced_search.is_valid():
paginator_queryset = self.get_component_queryset_from_advanced_search(
advanced_search.cleaned_data)
else:
paginator_queryset = Component.objects.all()
if parameter_formset.is_valid():
# Process parameters
pass
else:
search = self.request.GET.get('search', default=None)
paginator_queryset = self.get_component_query_set(search)
comp_paginator = Paginator(paginator_queryset, self.default_page_size)
context['components'] = comp_paginator.get_page(comp_page_num)
context['comp_form'] = ComponentForm()
context['import_comp_form'] = ImportComponentForm()
context['search_string'] = search
if not parameter_formset:
context['advanced_search_param_formset'] = ParameterSearchFormSet()
if not advanced_search:
context['advanced_search_form'] = AdvancedComponentSearchForm(auto_id='adv_search_%s')
return context
def get_context_data(self, **kwargs):
return self.get_context_data_int(advanced_search = None, parameter_formset=None, **kwargs)
def handle_new_component_post(self, request, open=False, **kwargs):
cform = ComponentForm(data=request.POST, files=request.FILES)
new_component = None
if cform.is_valid():
new_component = cform.save()
context = self.get_context_data(**kwargs)
if not cform.is_valid():
context['comp_form'] = cform
if open and new_component:
return redirect(reverse('parts-components-detail', kwargs={'uuid':new_component.id}))
return self.render_to_response(context)
def handle_import_components_post(self, request, open=False, **kwargs):
cform = ImportComponentForm(data=request.POST, files=request.FILES)
context = self.get_context_data(**kwargs)
if cform.is_valid():
try:
import_components_from_csv(cform.files['csv_file'].file)
except Exception as ex:
cform.add_error('csv_file', str(ex))
context['import_comp_form'] = cform
else:
context['import_comp_form'] = cform
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):
if 'submit-edit-component' in request.POST:
return self.handle_new_component_post(request, open=False, **kwargs)
elif 'submit-edit-component-open' in request.POST:
return self.handle_new_component_post(request, open=True, **kwargs)
elif 'submit-import-components' in request.POST:
return self.handle_import_components_post(request, open=True, **kwargs)
elif 'submit-advanced-search' in request.POST:
return self.handle_advanced_search_post(request, **kwargs)
else:
return super().post(request, *args, **kwargs)
class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
template_name = 'parts/components-detail.html'
model = Component
pk_url_kwarg = 'uuid'
base_title = ''
navbar_selected = 'Components'
def get_context_data(self, **kwargs):
self.base_title = 'Component / '+self.object.name
context = super().get_context_data(**kwargs)
context['component'] = self.object
context['stocks'] = Stock.objects.filter(component=self.object)
context['comp_form'] = ComponentForm(instance=self.object)
context['new_distri_num_form'] = DistributorNumberCreateForm()
context['new_param_form'] = ComponentParameterCreateForm()
context['distri_nums'] = DistributorNum.objects.filter(component=self.object).order_by(
'distributor__name')
context['parameters'] = ComponentParameter.objects.filter(component=self.object).order_by(
'parameter_type__parameter_name')
context['package_parameters'] = PackageParameter.objects.filter(package=self.object.package).order_by(
'parameter_type__parameter_name')
return context
def handle_submit_edit_component_post(self, request, **kwargs):
cform = ComponentForm(instance=self.object, data=request.POST, files=request.FILES)
if cform.is_valid():
cform.save()
context = self.get_context_data(**kwargs)
if not cform.is_valid():
context['comp_form'] = cform
return self.render_to_response(context)
def handle_submit_delete_post(self, request, **kwargs):
delete_error = None
protected_stuff = None
try:
self.object.delete()
except ProtectedError as pe:
delete_error = 'Component is protected'
protected_stuff = pe.protected_objects
except:
delete_error = 'Cannot delete component. Unknown error'
if delete_error is None:
return redirect('parts-components')
else:
context = self.get_context_data(**kwargs)
context['delete_error'] = delete_error
context['protected_stuff'] = protected_stuff
return self.render_to_response(context)
def handle_submit_new_distri_num_post(self, request, **kwargs):
form = DistributorNumberCreateForm(data=request.POST)
if form.is_valid():
new_number = form.save(commit=False)
new_number.component = self.object
try:
new_number.save()
except IntegrityError as ie:
form.add_error('__all__', 'Number for given distributor already exists')
context = self.get_context_data(**kwargs)
if not form.is_valid():
context['new_distri_num_form'] = form
return self.render_to_response(context)
def handle_submit_delete_distri_num_post(self, request, **kwargs):
form = DistributorNumberDeleteForm(data=request.POST)
if form.is_valid():
form.cleaned_data['distributor_num'].delete()
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def handle_submit_delete_param_post(self, request, **kwargs):
form = ComponentParameterDeleteForm(data=request.POST)
if form.is_valid():
form.cleaned_data['param_num'].delete()
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def handle_submit_new_param_post(self, request, **kwargs):
form = ComponentParameterCreateForm(data=request.POST)
if form.is_valid():
try:
form.save(self.object)
except IntegrityError:
form.add_error('__all__', 'This parameter is already set')
context = self.get_context_data(**kwargs)
if not form.is_valid():
context['new_param_form'] = form
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if 'submit-edit-component' in request.POST:
return self.handle_submit_edit_component_post(request, **kwargs)
elif 'submit-component-delete' in request.POST:
return self.handle_submit_delete_post(request, **kwargs)
elif 'submit-create-new-distri-num' in request.POST:
return self.handle_submit_new_distri_num_post(request, **kwargs)
elif 'submit-delete-distributor-num' in request.POST:
return self.handle_submit_delete_distri_num_post(request, **kwargs)
elif 'submit-delete-param' in request.POST:
return self.handle_submit_delete_param_post(request, **kwargs)
elif 'submit-create-new-param' in request.POST:
return self.handle_submit_new_param_post(request, **kwargs)
else:
return super().post(request, *args, **kwargs)

View File

@ -1,120 +0,0 @@
from django.shortcuts import redirect
from django.views.generic import TemplateView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from ..models import Distributor
from django.core.paginator import Paginator
from django.db.models import ProtectedError
from ..forms import *
from django.db.models import Q
from .generic_views import BaseTemplateMixin
class DistributorView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/distributors.html'
base_title = 'Distributors'
navbar_selected = 'Distributors'
default_page_size = 25
def search_distributors(self, search):
qs = Distributor.objects.all()
if not search:
return qs
search_fragments = search.strip().split()
for search in search_fragments:
qs = qs.filter(Q(name__icontains = search) | Q(website__icontains = search))
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
page_num = self.request.GET.get('page', default=1)
search_string = self.request.GET.get('search', default=None)
queryset = self.search_distributors(search_string)
paginator = Paginator(queryset, self.default_page_size)
context['search_string'] = search_string
context['distributors'] = paginator.get_page(page_num)
context['new_distri_form'] = DistributorForm()
return context
def handle_add_new_distributor(self, request):
form = DistributorForm(data=request.POST, files=request.FILES)
if form.is_valid():
form.save()
context = self.get_context_data()
if not form.is_valid():
context['new_distri_form'] = form
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
if 'submit-distri-add-new' in request.POST:
return self.handle_add_new_distributor(request)
return super().post(request, *args, **kwargs)
class DistributorDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
template_name = 'parts/distributors-detail.html'
model = Distributor
pk_url_kwarg = 'uuid'
base_title = ''
navbar_selected = 'Distributors'
def get_context_data(self, **kwargs):
self.base_title = 'Distributor / '+self.object.name
context = super().get_context_data(**kwargs)
context['distributor'] = self.object
context['edit_form'] = DistributorForm(instance=self.object)
return context
def handle_delete_distributor(self, request):
delete_error = None
protected_objects = None
# Try to delete this instance
try:
self.object.delete()
except ProtectedError as pe:
delete_error = 'Cannot delete this distributor. It is referenced by a component.'
protected_objects = pe.protected_objects
except:
delete_error = 'Cannot delete this distributor. Unknown error'
if delete_error:
context = self.get_context_data()
context['delete_error'] = delete_error
context['protected_components'] = protected_objects
return self.render_to_response(context)
else:
return redirect('parts-distributors')
def edit_distributor(self, request):
edit_form = DistributorForm(data=request.POST, files=request.FILES, instance=self.object)
if edit_form.is_valid():
edit_form.save()
context = self.get_context_data()
if not edit_form.is_valid():
context['edit_form'] = edit_form
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if 'submit-distri-delete' in request.POST:
return self.handle_delete_distributor(request)
elif 'submit-distri-edit' in request.POST:
return self.edit_distributor(request)
return super().post(request, *args, **kwargs)

View File

@ -1,121 +0,0 @@
from django.shortcuts import render, redirect
from django.contrib.auth import logout, login
from django.contrib.auth.forms import AuthenticationForm as AuthForm
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse
from ..navbar import NavBar
from ..forms import QrSearchForm
class BaseTemplateMixin():
navbar_selected = ''
base_title = ''
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
base_context = {
'navbar': NavBar.get_navbar(self.navbar_selected, self.request.user),
'title': NavBar.get_brand()+' / '+ self.base_title,
'login_active': False,
}
context['base'] = base_context
return context
def post(self, request, *args, **kwargs):
data = request.POST
if 'qr_search' not in data:
super().post(request, *args, **kwargs)
print('QR',data['qr_search'])
f = QrSearchForm(data)
if f.is_valid():
return redirect(f.my_qr_validator.get_redirect_url(f.cleaned_data['qr_search']))
return self.get(request)
class ChangePasswordView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/change-pw.html'
navbar_selected = 'Main'
base_title = 'Change Password'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = PasswordChangeForm(self.request.user)
return context
def post(self, request, *args, **kwargs):
if 'submit-change-pw' not in request.POST:
return super().post(request, *args, **kwargs)
form = PasswordChangeForm(request.user, data=request.POST)
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user)
return redirect('parts-main')
context = self.get_context_data(**kwargs)
if form.errors:
context['form'] = form
return self.render_to_response(context)
class MainView(BaseTemplateMixin, TemplateView):
template_name = 'parts/main.html'
navbar_selected = 'Main'
base_title = 'Main'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user'] = self.request.user
return context
def logout_view(request):
logout(request)
return redirect('parts-main')
def login_view(request):
base_context = {
'navbar': NavBar.get_navbar('Login', request.user),
'title': NavBar.get_brand()+' / '+'Login',
'login_active': True,
}
if request.user.is_authenticated:
next_param = request.GET.get('next')
if next_param is not None:
return redirect(next_param)
return redirect('parts-main')
if request.method == 'POST':
form = AuthForm(data=request.POST)
if form.is_valid():
valid_user = form.get_user()
login(request, valid_user)
next_param = request.GET.get('next')
if next_param is not None:
return redirect(next_param)
return redirect('parts-main')
else:
form = AuthForm()
context = {
'base': base_context,
'form': form,
}
return render(request, 'parts/login.html', context)
def health_check_view(_request) -> HttpResponse:
"""
Health checking view. Returns empty http response with HTTP status OK.
This will be used to check if the system is actually running correctly
"""
return HttpResponse(status=200)

View File

@ -1,121 +0,0 @@
from django.shortcuts import redirect
from django.views.generic import TemplateView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from ..models import Manufacturer
from django.core.paginator import Paginator
from django.db.models import ProtectedError
from ..forms import *
from django.db.models import Q
from .generic_views import BaseTemplateMixin
class ManufacturersView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/manufacturers.html'
base_title = 'Manufacturers'
navbar_selected = 'Manufacturers'
default_page_size = 25
def search_manufacturers(self, search):
qs = Manufacturer.objects.all()
if not search:
return qs
search_fragements = search.strip().split()
for search in search_fragements:
qs = qs.filter(Q(name__icontains = search) | Q(website__icontains = search))
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
page_num = self.request.GET.get('page', default=1)
search_string = self.request.GET.get('search', default=None)
queryset = self.search_manufacturers(search_string)
paginator = Paginator(queryset, self.default_page_size)
context['search_string'] = search_string
context['manufacturers'] = paginator.get_page(page_num)
context['new_manufacturer_form'] = ManufacturerForm()
return context
def handle_add_new_manufacturer(self, request):
form = ManufacturerForm(data=request.POST, files=request.FILES)
if form.is_valid():
form.save()
context = self.get_context_data()
if not form.is_valid():
context['new_manufacturer_form'] = form
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
if 'submit-manufacturer-add-new' in request.POST:
return self.handle_add_new_manufacturer(request)
return super().post(request, *args, **kwargs)
class ManufacturerDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
template_name = 'parts/manufacturers-detail.html'
model = Manufacturer
pk_url_kwarg = 'uuid'
base_title = ''
navbar_selected = 'Manufacturers'
def get_context_data(self, **kwargs):
self.base_title = 'Manufacturer / '+self.object.name
context = super().get_context_data(**kwargs)
context['manufacturer'] = self.object
context['edit_form'] = ManufacturerForm(instance=self.object)
return context
def handle_delete_manufacturer(self, request):
delete_error = None
protected_objects = None
# Try to delete this instance
try:
self.object.delete()
except ProtectedError as pe:
delete_error = 'Cannot delete this distributor. It is referenced by a component.'
protected_objects = pe.protected_objects
except:
delete_error = 'Cannot delete this distributor. Unknown error'
if delete_error:
context = self.get_context_data()
context['delete_error'] = delete_error
context['protected_components'] = protected_objects
return self.render_to_response(context)
else:
return redirect('parts-manufacturers')
def edit_manufacturer(self, request):
edit_form = ManufacturerForm(data=request.POST, files=request.FILES, instance=self.object)
if edit_form.is_valid():
edit_form.save()
context = self.get_context_data()
if not edit_form.is_valid():
context['edit_form'] = edit_form
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if 'submit-manufacturer-delete' in request.POST:
return self.handle_delete_manufacturer(request)
elif 'submit-manufacturer-edit' in request.POST:
return self.edit_manufacturer(request)
return super().post(request, *args, **kwargs)

View File

@ -1,156 +0,0 @@
from django.shortcuts import redirect
from django.views.generic import TemplateView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import Paginator
from django.db.models import ProtectedError
from django.db.models import Q
from ..forms import *
from ..models import Package, PackageParameter
from .generic_views import BaseTemplateMixin
class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/packages.html'
base_title = 'Packages'
navbar_selected = 'Packages'
default_page_size = 25
def search_packages(self, search):
qs = Package.objects.all()
if not search:
return qs
search_fragments = search.strip().split()
for search in search_fragments:
if search.lower() == 'smd':
s_filter = Q(name__icontains = search) | Q(smd = True)
else:
try:
pin_count = int(search)
s_filter = Q(name__icontains = search) | Q(pin_count=pin_count)
except:
s_filter = Q(name__icontains = search)
qs = qs.filter(s_filter)
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
page_num = self.request.GET.get('page', default=1)
search_string = self.request.GET.get('search', default=None)
package_queryset = self.search_packages(search_string)
paginator = Paginator(package_queryset, self.default_page_size)
context['search_string'] = search_string
context['packages'] = paginator.get_page(page_num)
context['new_pkg_form'] = PackageForm()
return context
def handle_add_new_package(self, request):
form = PackageForm(data=request.POST, files=request.FILES)
if form.is_valid():
form.save()
context = self.get_context_data()
if not form.is_valid():
context['new_pkg_form'] = form
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
if 'submit-pkg-add-new' in request.POST:
return self.handle_add_new_package(request)
return super().post(request, *args, **kwargs)
class PackageDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
template_name = 'parts/packages-detail.html'
model = Package
pk_url_kwarg = 'uuid'
base_title = ''
navbar_selected = 'Packages'
def get_context_data(self, **kwargs):
self.base_title = 'Package / '+self.object.name
context = super().get_context_data(**kwargs)
context['package'] = self.object
context['edit_form'] = PackageForm(instance=self.object)
context['new_param_form'] = PackageParameterCreateForm()
context['parameters'] = PackageParameter.objects.filter(package=self.object).order_by(
'parameter_type__parameter_name')
return context
def handle_delete_package(self, request):
delete_error = None
protected_objects = None
# Try to delete this instance
try:
self.object.delete()
except ProtectedError as pe:
delete_error = 'Cannot delete this package. It is referenced by a component.'
protected_objects = pe.protected_objects
except:
delete_error = 'Cannot delete this package. Unknown error'
if delete_error:
context = self.get_context_data()
context['delete_error'] = delete_error
context['protected_components'] = protected_objects
return self.render_to_response(context)
else:
return redirect('parts-packages')
def edit_package(self, request):
edit_form = PackageForm(data=request.POST, files=request.FILES, instance=self.object)
if edit_form.is_valid():
edit_form.save()
context = self.get_context_data()
if not edit_form.is_valid():
context['edit_form'] = edit_form
return self.render_to_response(context)
def handle_submit_delete_param_post(self, request, **kwargs):
form = PackageParameterDeleteForm(data=request.POST)
if form.is_valid():
form.cleaned_data['param_num'].delete()
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def handle_submit_new_param_post(self, request, **kwargs):
form = PackageParameterCreateForm(data=request.POST)
if form.is_valid():
try:
form.save(self.object)
except IntegrityError:
form.add_error('__all__', 'This parameter is already set')
context = self.get_context_data(**kwargs)
if not form.is_valid():
context['new_param_form'] = form
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if 'submit-pkg-delete' in request.POST:
return self.handle_delete_package(request)
elif 'submit-pkg-edit' in request.POST:
return self.edit_package(request)
elif 'submit-delete-param' in request.POST:
return self.handle_submit_delete_param_post(request, **kwargs)
elif 'submit-create-new-param' in request.POST:
return self.handle_submit_new_param_post(request, **kwargs)
return super().post(request, *args, **kwargs)

View File

@ -1,289 +0,0 @@
import uuid
from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic import TemplateView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import Paginator
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.db.models.functions import Lower
from ..models import Storage, Stock
from ..forms import *
from .generic_views import BaseTemplateMixin
class StockView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/stocks.html'
base_title = 'Stocks'
navbar_selected = 'Stocks'
default_pagination_size = 25
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
storage_page = self.request.GET.get('storage_page')
if storage_page is None:
storage_page = 1
low_stock_page = self.request.GET.get('low_stock_page')
if low_stock_page is None:
low_stock_page = 1
storage_paginator = Paginator(Storage.objects.filter(parent_storage=None), self.default_pagination_size)
low_stock_paginator = Paginator(Stock.get_under_watermark(),
self.default_pagination_size)
context['low_stocks'] = low_stock_paginator.get_page(low_stock_page)
context['storages'] = storage_paginator.get_page(storage_page)
add_stor_form = AddSubStorageForm()
add_stor_form.fields['responsible'].initial = self.request.user.id
context['add_storage_form'] = add_stor_form
return context
def handle_add_storage(self, request, **kwargs):
f = AddSubStorageForm(data=request.POST)
if f.is_valid():
sub_name = f.cleaned_data['storage_name']
try:
Storage.objects.create(name=sub_name,
responsible=f.cleaned_data['responsible'],
is_template=f.cleaned_data['is_template'],
template=f.cleaned_data.get('template'))
except ValidationError as v_err:
f.add_error('storage_name', '. '.join(v_err.messages))
context = self.get_context_data(**kwargs)
context['add_storage_form'] = f
return self.render_to_response(context)
def post(self, request, **kwargs):
if 'submit-add-storage' in request.POST:
return self.handle_add_storage(request, **kwargs)
return super().post(request, **kwargs)
class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
template_name = 'parts/stocks-detail.html'
model = Storage
pk_url_kwarg = 'uuid'
base_title = ''
navbar_selected = 'Stocks'
default_pagination_size = 8
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 search_stock_queryset(self, search):
if self.object.expand_sub_storage_stocks:
stocks_in_storage = Stock.objects.filter(storage__in=self.object.get_storage_list())
else:
stocks_in_storage = Stock.objects.filter(storage=self.object)
stocks_in_storage = stocks_in_storage.order_by(Lower('component__name'))
if search is None or search == '':
return stocks_in_storage
if search.startswith('[comp_uuid]'):
search = search.replace('[comp_uuid]', '')
# Check if the searhc equals a UUID
test_uuid = None
try:
test_uuid = uuid.UUID(search)
except:
pass
if test_uuid is not None:
stocks_in_storage = stocks_in_storage.filter(Q(component__id = test_uuid) | Q(id= test_uuid))
else:
stocks_in_storage = stocks_in_storage.filter(Q(component__name__icontains = search) |
Q(component__package__name__icontains = search) |
Q(component__manufacturer__name__icontains = search))
return stocks_in_storage
def get_context_data(self, **kwargs):
self.base_title = 'Stocks / ' + self.object.name
context = super().get_context_data(**kwargs)
context['breadcrumbs'] = self.get_breadcrumbs()
storage_page = self.request.GET.get('storage_page', default=1)
storage_paginator = Paginator(Storage.objects.filter(parent_storage=self.object), self.default_pagination_size)
stock_search_input = self.request.GET.get('search')
componente_stock_page = self.request.GET.get('stock_page', default=1)
stock_paginator = Paginator(self.search_stock_queryset(stock_search_input), self.default_pagination_size)
context['storages'] = storage_paginator.get_page(storage_page)
stocks = stock_paginator.get_page(componente_stock_page)
context['stocks'] = stocks
context['stocks_with_forms'] = [{'object': s, 'relocate_form': RelocateStockForm(instance=s, prefix=str(s.id))} for s in stocks]
context['stock_search'] = stock_search_input
add_storage_form = AddSubStorageForm()
add_storage_form.fields['responsible'].initial = self.request.user.id
context['add_storage_form'] = add_storage_form
change_storage_form = ChangeStorageForm(prefix='change_storage')
change_storage_form.fields['storage_name'].initial = self.object.name
change_storage_form.fields['verbose_name'].initial = self.object.verbose_name
change_storage_form.fields['responsible'].initial = self.object.responsible.id
change_storage_form.fields['expand_sub_storage_stocks'].initial = self.object.expand_sub_storage_stocks
change_storage_form.fields['is_template'].initial = self.object.is_template
context['change_storage_form'] = change_storage_form
context['delete_storage_error'] = None
context['add_stock_form'] = AddStockForm()
return context
def handle_add_storage_post(self, request, **kwargs):
f = AddSubStorageForm(data=request.POST)
if f.is_valid():
sub_name = f.cleaned_data['storage_name']
try:
Storage.objects.create(name=sub_name,
verbose_name=f.cleaned_data.get('verbose_name'),
parent_storage=self.object,
responsible=f.cleaned_data['responsible'],
expand_sub_storage_stocks=f.cleaned_data['expand_sub_storage_stocks'],
is_template=f.cleaned_data['is_template'],
template=f.cleaned_data.get('template'))
except ValidationError as v_err:
f.add_error('storage_name', '. '.join(v_err.messages))
context = self.get_context_data(**kwargs)
context['add_storage_form'] = f
return self.render_to_response(context)
def handle_change_storage_post(self, request, **kwargs):
f = ChangeStorageForm(data=request.POST, prefix='change_storage')
if f.is_valid():
try:
self.object.name = f.cleaned_data['storage_name']
self.object.verbose_name = f.cleaned_data.get('verbose_name')
self.object.responsible = f.cleaned_data['responsible']
self.object.expand_sub_storage_stocks = f.cleaned_data['expand_sub_storage_stocks']
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):
parent = self.object.parent_storage
try:
self.object.delete()
except:
context = self.get_context_data(**kwargs)
context['delete_storage_errors'] = ['Error deleting Storage '+str(self.object)]
return self.render_to_response(context)
if parent is None:
return redirect('parts-stocks')
else:
return redirect(reverse('parts-stocks-detail', kwargs={'uuid':parent.id}))
def handle_del_stock_post(self, request, **kwargs):
del_error = None # TODO: Check error handling. This is clearly not working as intended :P
if 'stock_uuid' in request.POST:
f = DeleteStockForm(data=request.POST)
if f.is_valid():
try:
s = Stock.objects.get(id=f.cleaned_data['stock_uuid'])
print(s.storage)
print(self.object)
if s.storage == self.object:
s.delete()
else:
del_error = 'Cannot delete stock from another storage.'
except:
del_error = 'Could not find requested stock in this storage.'
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def handle_update_watermark(self, request, **kwargs):
edit_form = EditWatermarkForm(data=request.POST)
if edit_form.is_valid():
edit_form.save()
else:
pass # Todo: Handle error
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def handle_update_lot(self, request, **kwargs):
edit_form = EditLotForm(data=request.POST)
if edit_form.is_valid():
edit_form.save()
else:
pass # Todo: Handle error
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def handle_relocate_stock(self, request, **kwargs):
instance = Stock.objects.get(id=request.POST['prefix'])
edit_form = RelocateStockForm(instance=instance, data=request.POST, prefix=request.POST['prefix'])
if edit_form.is_valid():
edit_form.save()
else:
pass # Todo: Handle error
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def handle_amount_change_post(self, request, increase, **kwargs):
edit_form = EditStockAmountForm(data=request.POST)
if edit_form.is_valid():
edit_form.save(increase)
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def handle_add_stock_post(self, request, **kwargs):
f = AddStockForm(data=request.POST)
error_occured = False
if f.is_valid():
try:
f.save(self.object)
except Exception as ex:
f.add_error('', str(ex))
error_occured = True
else:
error_occured = True
context = self.get_context_data(**kwargs)
if error_occured:
context['add_stock_form'] = f
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if 'submit-add-storage' in request.POST:
return self.handle_add_storage_post(request, **kwargs)
elif 'submit-change-storage' in request.POST:
return self.handle_change_storage_post(request, **kwargs)
elif 'submit-delete-storage' in request.POST:
return self.handle_del_storage_post(request, **kwargs)
elif 'submit-delete-stock' in request.POST:
return self.handle_del_stock_post(request, **kwargs)
elif 'submit-edit-watermark' in request.POST:
return self.handle_update_watermark(request, **kwargs)
elif 'submit-edit-lot' in request.POST:
return self.handle_update_lot(request, **kwargs)
elif 'submit-relocate-stock' in request.POST:
return self.handle_relocate_stock(request, **kwargs)
elif 'submit-amount-reduce' in request.POST:
return self.handle_amount_change_post(request, False, **kwargs)
elif 'submit-amount-increase' in request.POST:
return self.handle_amount_change_post(request, True, **kwargs)
elif 'submit-add-stock' in request.POST:
return self.handle_add_stock_post(request, **kwargs)
return super().post(request, *args, **kwargs)

View File

@ -16,13 +16,9 @@ The following environment variables have to be set:
- DJANGO_MEDIA_URL - DJANGO_MEDIA_URL
- DJANGO_MEDIA_ROOT - DJANGO_MEDIA_ROOT
- DJANGO_POSTGRESQL_SOCKET - DJANGO_POSTGRESQL_SOCKET
- DJANGO_POSTGRESQL_PORT
The following can be set The following can be set
- DJANGO_POSTGRESQL_PW (assumed empty if missing)
- DJANGO_POSTGRESQL_USER (assmumed empty if mssing)
- DJANGO_SECURE_HSTS_SECONDS (defaults to 120) - DJANGO_SECURE_HSTS_SECONDS (defaults to 120)
- DJANGO_FORCE_DEV_MODE
""" """
@ -50,11 +46,9 @@ 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 = False DEBUG = False
if get_env_value('DJANGO_FORCE_DEV_MODE', default=False) == 'True':
DEBUG = True
ALLOWED_HOSTS = ['localhost', get_env_value('DJANGO_ALLOWED_HOST')]
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', get_env_value('DJANGO_ALLOWED_HOST')]
# Application definition # Application definition
@ -144,7 +138,6 @@ DATABASES = {
'USER': db_user, 'USER': db_user,
'PASSWORD': db_pw, 'PASSWORD': db_pw,
'HOST': get_env_value('DJANGO_POSTGRESQL_SOCKET'), 'HOST': get_env_value('DJANGO_POSTGRESQL_SOCKET'),
'PORT': get_env_value('DJANGO_POSTGRESQL_PORT'),
} }
} }
@ -236,9 +229,6 @@ CRISPY_TEMPLATE_PACK = "bootstrap5"
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = False SECURE_SSL_REDIRECT = True
# allow detection of https behind "old" nginx
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_HSTS_SECONDS = get_env_value('DJANGO_SECURE_HSTS_SECONDS', default=120) SECURE_HSTS_SECONDS = get_env_value('DJANGO_SECURE_HSTS_SECONDS', default=120)

View File

@ -14,13 +14,14 @@ 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, re_path from django.urls import path, include
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 = [
re_path(r'^admin/login/', parts_views.login_view), url(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

@ -31,7 +31,7 @@ class EngineeringNumberConverter():
] ]
@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)

View File

@ -1,2 +0,0 @@
name;manufacturer;component_type;pref_distri;description;datasheet_link;package;image_url;storage_uuid;substorage_path;amount;lot;watermark;param:Resistance;param:Temperature coefficient;param:Tolerance;distri:Mouser
1K41;Phycomp;Resistor;;KVG - expired;;R0603;https://t3.ftcdn.net/jpg/02/95/44/22/360_F_295442295_OXsXOmLmqBUfZreTnGo9PREuAPSLQhff.jpg;a4909bb4-4d69-4e58-8fbd-fba4fa62d9fb;bew;1111;4000;0;1330;100;1;gggg
1 name manufacturer component_type pref_distri description datasheet_link package image_url storage_uuid substorage_path amount lot watermark param:Resistance param:Temperature coefficient param:Tolerance distri:Mouser
2 1K41 Phycomp Resistor KVG - expired R0603 https://t3.ftcdn.net/jpg/02/95/44/22/360_F_295442295_OXsXOmLmqBUfZreTnGo9PREuAPSLQhff.jpg a4909bb4-4d69-4e58-8fbd-fba4fa62d9fb bew 1111 4000 0 1330 100 1 gggg

View File

@ -1,14 +0,0 @@
# The shimatta kenkyusho can import components from a CSV file uploaded to the website
All parameters are passed as names (e.g. package, manufacturer etc.).
Component parameters can be set using a prefix ``param:`` followed by the
parameter name. The value is then parsed bysed on the parameter type.
Distributor part numbers can be set similarly by prepending ``distri:``.
It is also possible to create initial stocks to one storage by passing the
storage uuid and/or the path to the storage (as printed in the breadcrumbs) or
a combination of a storage uuid and the path from this storage.
See example for details.

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);
@ -27,17 +27,6 @@ function initialize_autocompletion_foreign_key_field(search_element) {
}); });
} }
// select first match if any on enter
search_element.addEventListener('keydown', (e) => {
if (e.key === "Enter") {
e.preventDefault();
first_search_result = search_element.parentElement.querySelector('li')
if (first_search_result) {
first_search_result.click()
}
}
});
new AutocompleteCustomUi( new AutocompleteCustomUi(
base_id, base_id+'-ac-ul', function(search_query, autocomplete_obj) { base_id, base_id+'-ac-ul', function(search_query, autocomplete_obj) {
api_ajax_request_without_send('GET', search_url+`?search=${encodeURIComponent(search_query)}`, function(method, url, json) { api_ajax_request_without_send('GET', search_url+`?search=${encodeURIComponent(search_query)}`, function(method, url, json) {

View File

@ -38,9 +38,7 @@ class AutocompleteCustomUi {
this.query_callback = query_function.bind(this); this.query_callback = query_function.bind(this);
document.getElementById(text_id).addEventListener("keyup", this.ac_delay(function(event) { document.getElementById(text_id).addEventListener("keyup", this.ac_delay(function(event) {
if (event.key != 'Enter') {
this.query_callback(document.getElementById(this.text_id).value, this); this.query_callback(document.getElementById(this.text_id).value, this);
}
}, autocomplete_query_delay_ms).bind(this)); }, autocomplete_query_delay_ms).bind(this));
this.dropdown_data = {}; this.dropdown_data = {};

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' %}',
@ -75,29 +74,9 @@
'component-parameter-type-list': '{% url 'componentparametertype-list' %}', 'component-parameter-type-list': '{% url 'componentparametertype-list' %}',
}; };
</script> </script>
<script type="text/javascript" src="{% static 'js/kenkyusho-api-v1.js' %}"></script> <script type="text/javascript" src="{% static 'js/kenyusho-api-v1.js' %}"></script>
<script type="text/javascript" src="{% static 'js/autocomplete.js' %}"></script> <script type="text/javascript" src="{% static 'js/autocomplete.js' %}"></script>
<script type="text/javascript" src="{% static 'js/autocomplete-foreign-key-field.js' %}"></script> <script type="text/javascript" src="{% static 'js/autocomplete-foreign-key-field.js' %}"></script>
<!-- Initialize bootstrap popovers -->
<script type="text/javascript">
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
</script>
<!-- Initialize bootstrap tooltips -->
<script type="text/javascript">
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
const tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
</script>
<!-- Select search field on start of QR scan if no input is currently selevted([) -->
<script type="text/javascript">
window.addEventListener("keydown", (event)=>{
if (document.activeElement.nodeName != 'INPUT' && event.key == '[') {
document.getElementById("qr_search_field").focus()
}
})
</script>
{% block custom_scripts %} {% block custom_scripts %}
{% endblock custom_scripts %} {% endblock custom_scripts %}

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 %}
@ -76,11 +76,6 @@
No description available No description available
</div> </div>
{% endif %} {% endif %}
{% if component.dynamic_description %}
<h2>Dynamic Description</h2>
<pre>{{ component.dynamic_description }}</pre>
{% endif %}
</div> </div>
<div class="col-4"> <div class="col-4">
{% if component.pref_distri %} {% if component.pref_distri %}
@ -125,7 +120,6 @@
</thead> </thead>
<tbody> <tbody>
{% for param in package_parameters %} {% for param in package_parameters %}
<tr>
<td> <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 %}> <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}} {{param.parameter_type.parameter_name}}
@ -134,8 +128,7 @@
<td> <td>
{{param.resolved_value_as_string}} {{param.resolved_value_as_string}}
</td> </td>
<td><span class="text-info">from Package</span></td> <td><span class="text-secondary">from Package</span></td>
</tr>
{% endfor %} {% endfor %}
{% for param in parameters %} {% for param in parameters %}
<tr> <tr>

View File

@ -14,11 +14,10 @@
</button> </button>
<button class="btn btn-secondary" type="button" data-bs-toggle="collapse" href="#advanced-search-collapse">Advanced <i class="bi bi-search"></i></button> <button class="btn btn-secondary" type="button" data-bs-toggle="collapse" href="#advanced-search-collapse">Advanced <i class="bi bi-search"></i></button>
<button class="btn btn-success" type="button" data-bs-toggle="modal" data-bs-target="#comp-edit-modal"><i class="bi bi-plus-circle"></i> Add Component</button> <button class="btn btn-success" type="button" data-bs-toggle="modal" data-bs-target="#comp-edit-modal"><i class="bi bi-plus-circle"></i> Add Component</button>
<button class="btn btn-success" type="button" data-bs-toggle="modal" data-bs-target="#comp-import-modal"><i class="bi bi-plus-circle"></i> Import CSV</button>
</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 %}
@ -32,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 %}
@ -46,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 %}
@ -54,12 +61,7 @@
Manufacturer: {{comp.manufacturer}} Manufacturer: {{comp.manufacturer}}
{% endif %} {% endif %}
</div> </div>
<div class="flex-grow-1 d-block ms-3" style="text-align: right;"> <span class="badge bg-primary rounded-pill">{{comp.get_total_amount}}</span>
<pre>{{ comp.dynamic_description }}</pre>
</div>
<div style="width: 10%; text-align: right;">
<span class="badge bg-primary rounded-pill me-2">{{comp.get_total_amount}}</span>
</div>
</li> </li>
</a> </a>
{% endfor %} {% endfor %}
@ -70,7 +72,6 @@
</div> </div>
{% include 'parts/modals/edit-component-modal.html' with form=comp_form heading='New Component' open_component_button=True %} {% include 'parts/modals/edit-component-modal.html' with form=comp_form heading='New Component' open_component_button=True %}
{% include 'parts/modals/import-component-modal.html' with form=import_comp_form %}
{% endblock content %} {% endblock content %}
{% block custom_scripts %} {% block custom_scripts %}
@ -79,8 +80,5 @@
{% if comp_form.errors %} {% if comp_form.errors %}
bootstrap.Modal.getOrCreateInstance(document.getElementById('comp-edit-modal')).show() bootstrap.Modal.getOrCreateInstance(document.getElementById('comp-edit-modal')).show()
{% endif %} {% endif %}
{% if import_comp_form.errors %}
bootstrap.Modal.getOrCreateInstance(document.getElementById('comp-import-modal')).show()
{% endif %}
</script> </script>
{% endblock custom_scripts %} {% endblock custom_scripts %}

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

@ -1,38 +0,0 @@
{% load static %}
{% load crispy_forms_tags %}
<div class="modal fade" id="comp-import-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h2>Import Component CSV</h2>
<button type="button"
class="btn"
data-bs-toggle="popover"
data-bs-title="Import Component CSV"
data-bs-html="true"
data-bs-content="The shimatta kenkyusho can import components from a CSV file uploaded to the website.<br>
All parameters are passed as names (e.g. package, manufacturer etc.).<br><br>
Component parameters can be set using a prefix <code>param:</code> followed by the
parameter name. The value is then parsed bysed on the parameter type.<br><br>
Distributor part numbers can be set similarly by prepending <code>distri:</code>.<br><br>
It is also possible to create initial stocks to one storage by passing the
storage uuid and/or the path to the storage (as printed in the breadcrumbs) or
a combination of a storage uuid and the path from this storage.<br><br>
See <a href='{% static 'example/import_csv/import_csv.csv' %}'>example</a> for details.">
<i class="bi bi-info-circle"></i>
</button>
<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">
<input type="submit" name="submit-import-components" class="btn btn-primary" value="Save">
</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

@ -47,21 +47,6 @@ needs following context:
<input type="submit" class="btn btn-primary" name="submit-edit-watermark" value="Update Watermark"> <input type="submit" class="btn btn-primary" name="submit-edit-watermark" value="Update Watermark">
</div> </div>
</form> </form>
<form method="post">
{% csrf_token %}
<input type="hidden" name="stock_uuid" value="{{stock.id}}">
<div class="input-group mb-3">
<input type="text" name="lot" id="ch-stk-lot-{{stock.id}}" class="form-control" value="{{stock.lot}}" required>
<input type="submit" class="btn btn-primary" name="submit-edit-lot" value="Update Lot">
</div>
</form>
<form method="post">
{% csrf_token %}
<input type="hidden" name="prefix" value="{{relocate_form.prefix}}">
{{ relocate_form|crispy }}
<input type="submit" class="btn btn-warning" name="submit-relocate-stock" value="Relocate Stock">
</form>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -26,46 +26,6 @@
<input type="submit" class="btn btn-primary" value="Save" name="submit-pkg-edit"> <input type="submit" class="btn btn-primary" value="Save" name="submit-pkg-edit">
</form> </form>
</div> </div>
<div class="col-md-3">
<h3>Parameters <button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#new-component-parameter-modal"><i class="bi bi-plus-circle"></i></button></h3>
<table class="table align-middle mb-3">
<thead>
<th scope="col">Parameter</th>
<th scope="col">Value</th>
<th scope="col"></th>
</thead>
<tbody>
{% for param in parameters %}
<tr>
<td>
<h6 {% if param.parameter_type.parameter_description %} class="accordion-header" data-bs-toggle="collapse" data-bs-target="#collapse-parameter-desc-{{forloop.counter}}"{% endif %}>
{{param.parameter_type.parameter_name}}
</h6>
</td>
<td>
{{param.resolved_value_as_string}}
</td>
<td>
<form method="post">
{% csrf_token %}
<input type="hidden" value="{{param.id}}" name="param_num">
<button class="btn btn-danger" name="submit-delete-param">X</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="accordion" id="accordion-param-desc">
{% for param in parameters %}
{% if param.parameter_type.parameter_description %}
<div class="collapse accordion-collapse" id="collapse-parameter-desc-{{forloop.counter}}" data-bs-parent="#accordion-param-desc">
{{param.parameter_type.parameter_description}}
</div>
{% endif %}
{% endfor %}
</div>
</div>
</div> </div>
</div> </div>
@ -120,7 +80,7 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'parts/modals/new-component-parameter-modal.html' with component_name=object.name form=new_param_form %}
{% endblock content %} {% endblock content %}

View File

@ -2,7 +2,6 @@
{% load qr_code %} {% load qr_code %}
{% load static %} {% load static %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load storage_tags %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
@ -21,44 +20,23 @@
<div class="row"> <div class="row">
{% qr_from_text object.get_qr_code size="m" image_format="svg" %} {% qr_from_text object.get_qr_code size="m" image_format="svg" %}
</div> </div>
<div class="row">
<h4>{{storage.name}}
{% if storage.verbose_name %}
<small>
({{storage.verbose_name}})
</small>
{% endif %}
{% if storage.is_template %}
<small>
(template)
</small>
{% endif %}
</h4>
</div>
<div class="row"> <div class="row">
{% if object.parent_storage %} {% if object.parent_storage %}
<h1>Sub-Storages <a class="btn btn-secondary" href="{% url 'parts-stocks-detail' uuid=object.parent_storage.id %}">Parent Storage</a> <h1>Sub-Storages <a class="btn btn-secondary" href="{% url 'parts-stocks-detail' uuid=object.parent_storage.id %}">Parent Storage</a> {% else %}
{% else %}
<h1>Sub-Storages <a class="btn btn-secondary" href="{% url 'parts-stocks'%}">Stock Overview</a> <h1>Sub-Storages <a class="btn btn-secondary" href="{% url 'parts-stocks'%}">Stock Overview</a>
{% endif %} {% endif %}
{% if storage.template %}
<a class="btn btn-secondary" href="{% url 'parts-stocks-detail' uuid=storage.template.id %}">Template</a>
{% 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-pencil-square"></i></button>
</h1> </h1>
<div class="list-group"> <div class="list-group">
{% for storage in storages %} {% for storage in storages %}
<a href="{% url 'parts-stocks-detail' uuid=storage.id %}" class="text-decoration-none"> <a href="{% url 'parts-stocks-detail' uuid=storage.id %}" class="text-decoration-none">
<li class="list-group-item list-group-item-action justify-content-between align-items-center d-flex"> <li class="list-group-item list-group-item-action justify-content-between align-items-center d-flex">
<div> <div>
<h5>{{storage.name}}{% if storage.verbose_name %}<small> ({{storage.verbose_name}})</small>{% endif %}</h5> <h5>{{storage.name}}</h5>
Responsible: {{ storage.responsible }} Responsible: {{ storage.responsible }}
</div> </div>
<span class="badge ms-1 bg-primary rounded-pill" data-bs-toggle="tooltip" data-bs-placement="top" title="Total number of stored parts">{{storage.get_total_stock_amount}}</span> <span class="badge bg-primary rounded-pill">{{storage.get_total_stock_amount}}</span>
<span class="badge ms-1 bg-secondary rounded-pill d-none d-lg-block" data-bs-toggle="tooltip" data-bs-placement="top" title="Number of stored lots">{{storage.get_total_stock_count}}</span>
<span class="badge ms-1 bg-info rounded-pill d-none d-lg-block" data-bs-toggle="tooltip" data-bs-placement="top" title="Number of substorages">{{storage.get_total_substorage_amount}}</span>
</li> </li>
</a> </a>
{% endfor %} {% endfor %}
@ -91,27 +69,22 @@
{% 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 %}
{% if stock.component.manufacturer %} {% if stock.component.manufacturer %}
Manufacturer: {{stock.component.manufacturer}} Manufacturer: {{stock.component.manufacturer}}
{% endif %} {% endif %}
{% if stock.storage != storage %}
<span class="text-secondary"><br>{{ stock.storage|get_relative_storage_path:storage }}</span>
{% endif %}
{% if stock.lot %} {% if stock.lot %}
<span class="text-secondary"><br>Lot: {{stock.lot}}</span> <span class="text-secondary"><br>Lot: {{stock.lot}}</span>
{% endif %} {% endif %}
</div> </div>
<div class="flex-grow-2 ms-3 d-none d-lg-block" style="text-align: right;"> <div class="ms-3">
<pre>{{ stock.component.dynamic_description }}</pre>
</div>
<div class="flex-grow-2 ms-5 d-none d-lg-block">
{% qr_from_text stock.get_qr_code size="6" image_format="svg" %}
</div>
<div class="ms-3" style="width: 20%;">
Amount: {{stock.amount}} Amount: {{stock.amount}}
{% if stock.watermark >= 0 %} {% if stock.watermark >= 0 %}
<br>Watermark: {{stock.watermark}} <br>Watermark: {{stock.watermark}}
@ -135,16 +108,12 @@
{% include 'paginator.html' with paginator=stocks get_param='stock_page' aria_label='Stock Page Navigation' %} {% include 'paginator.html' with paginator=stocks get_param='stock_page' aria_label='Stock Page Navigation' %}
</div> </div>
</div> </div>
{% for stock in stocks_with_forms %} {% for stock in stocks %}
{% include 'parts/modals/update-stock-modal.html' with stock=stock.object form=change_stock_form relocate_form=stock.relocate_form %} {% include 'parts/modals/update-stock-modal.html' with stock=stock form=change_stock_form %}
{% 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 storage-->
{% 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 %}
@ -193,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

@ -40,7 +40,7 @@
<a href="{% url 'parts-stocks-detail' uuid=storage.id %}" class="text-decoration-none"> <a href="{% url 'parts-stocks-detail' uuid=storage.id %}" class="text-decoration-none">
<li class="list-group-item list-group-item-action justify-content-between align-items-center d-flex"> <li class="list-group-item list-group-item-action justify-content-between align-items-center d-flex">
<div> <div>
<h5>{{storage.name}}{% if storage.verbose_name %}<small> ({{storage.verbose_name}})</small>{% endif %}</h5> <h5>{{storage.name}}</h5>
Responsible: {{ storage.responsible }} Responsible: {{ storage.responsible }}
</div> </div>
<span class="badge bg-primary rounded-pill">{{storage.get_total_stock_amount}}</span> <span class="badge bg-primary rounded-pill">{{storage.get_total_stock_amount}}</span>
@ -54,7 +54,7 @@
<!-- Add storage modal form --> <!-- Add storage modal form -->
{% 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 %} {% 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">
{% if custom.prepend %}
<span class="input-group-text" id="{{widget.attrs.id}}-prepend">{{custom.prepend}}</span>
{% 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}}"> <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 id="{{widget.attrs.id}}-ac-ul" class="dropdown-menu">
</ul> </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,7 +0,0 @@
#!/bin/bash
# Startup the db container
docker compose start shimatta-kenkyusho-db
# Override entrypoint to get interactive shell
docker compose run --entrypoint="/bin/sh" -p 8000:8000 shimatta-kenkyusho-web

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