Compare commits
108 Commits
port-to-dj
...
develop
Author | SHA1 | Date | |
---|---|---|---|
8b3ef1af25 | |||
1b48e8f283 | |||
6dc8f3bfef | |||
871086c7b7 | |||
adf152938d | |||
462ed0c101 | |||
032c4fc838 | |||
e0c39d9d6e | |||
6ca0ce483f | |||
35b99e1b18 | |||
6ae94e9ea4 | |||
aefcc472ea | |||
7e36059605 | |||
ed1508f0ed | |||
2d78b4dcdd | |||
19852dd5ad | |||
3ec11cf092 | |||
e4e7456a5d | |||
6eaef98c86 | |||
3aa4225acb | |||
39b64aeb71 | |||
f6a878460d | |||
171b6b83f4 | |||
550e996ae7 | |||
122ae4a731 | |||
0b26a81b94 | |||
befd5e452f | |||
cfb9970c26 | |||
146c2da4f3 | |||
e74a28b0a8 | |||
46a94c9688 | |||
21ac4a8c06 | |||
f2a8166874 | |||
15b4257c73 | |||
c19f4a8159 | |||
26b001d983 | |||
207b5c3fb5 | |||
3450f7475a | |||
2fdcfe8baf | |||
c1b9c966dd | |||
f202896c92 | |||
50cfe0a2c6 | |||
2d718c5e3a | |||
57b475cbe1 | |||
25b592ee39 | |||
08a5f97fd4 | |||
511dacf54a | |||
0c4f1f9dba | |||
08bae61fc0 | |||
4ff71d2b21 | |||
b47c7ad38d | |||
63b8a66ebb | |||
b873b1fd0f | |||
6e51085210 | |||
5163834de4 | |||
39e40d62f4 | |||
841be4f1bb | |||
0d4019832f | |||
fed63fd45f | |||
d10c48f2b9 | |||
a54cd33880 | |||
7c1465428f | |||
9afa7d709b | |||
f2ea45cd19 | |||
7678e6ad88 | |||
5ce7d99db2 | |||
5109f8d094 | |||
6d0ec7e448 | |||
8422ff0eeb | |||
1f3ed7f8ed | |||
b3f8041f08 | |||
173f0e3c91 | |||
a2f96ed4f0 | |||
741a634546 | |||
74e0be71b9 | |||
ce00f018fd | |||
6c6ef9e5fc | |||
b9d788935d | |||
aa08de0d10 | |||
4ee55c1675 | |||
9adafeb882 | |||
f1e366c7af | |||
390eaac396 | |||
3d63357534 | |||
1c08f433d4 | |||
8a8f2cdea4 | |||
20a83c7e91 | |||
66f4eea77d | |||
561b2aed27 | |||
a59fad4866 | |||
144a65ee05 | |||
fc5fa9f740 | |||
d2ce635f05 | |||
64d0a1bfb3 | |||
b057fedb5f | |||
254bf2bdf0 | |||
6e01b1939d | |||
a77f46d697 | |||
50ecaa2cc0 | |||
3e72cfe5d8 | |||
5e478ba624 | |||
8280fe7116 | |||
2e295e4691 | |||
0b27e9f064 | |||
6dd781021c | |||
7e0fc86b7f | |||
7a1bf054f5 | |||
5b6f4e16ef |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
start_server.sh
|
||||
run/*
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -125,3 +125,6 @@ dmypy.json
|
||||
|
||||
|
||||
myenv/*
|
||||
|
||||
run/*
|
||||
.env
|
||||
|
@ -6,10 +6,9 @@
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Django",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"pythonPath": "${workspaceFolder}/../myenv/bin/python",
|
||||
"program": "${workspaceFolder}/manage.py",
|
||||
"program": "${workspaceFolder}/shimatta_kenkyusho/manage.py",
|
||||
"args": [
|
||||
"runserver",
|
||||
"0.0.0.0:8000"
|
23
Dockerfile
23
Dockerfile
@ -1,17 +1,6 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
RUN apt-get update && apt-get install -y libpq-dev python3 python3-poetry nginx
|
||||
COPY shimatta_kenkyusho /app/shimatta_kenkyusho
|
||||
COPY poetry.lock /app/
|
||||
COPY pyproject.toml /app/
|
||||
RUN systemctl enable nginx
|
||||
|
||||
ARG UNAME=django
|
||||
ARG UID=1000
|
||||
ARG GID=1000
|
||||
RUN groupadd -g $GID -o $UNAME
|
||||
RUN useradd -m -u $UID -g $GID -o -s /bin/bash $UNAME
|
||||
USER $UNAME
|
||||
|
||||
WORKDIR "/app"
|
||||
RUN poetry install
|
||||
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
Normal file
192
README.md
Normal file
@ -0,0 +1,192 @@
|
||||
# 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
|
||||
|
54
compose.yaml
Normal file
54
compose.yaml
Normal file
@ -0,0 +1,54 @@
|
||||
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:
|
8
entrypoint.sh
Executable file
8
entrypoint.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/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
|
7
entrypoint_self_hosted.sh
Executable file
7
entrypoint_self_hosted.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/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
|
38
example.env
Normal file
38
example.env
Normal file
@ -0,0 +1,38 @@
|
||||
####################################################################################################
|
||||
# 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
|
226
poetry.lock
generated
226
poetry.lock
generated
@ -1,226 +0,0 @@
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.5.0"
|
||||
description = "Reusable constraint types to use with typing.Annotated"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.7.2"
|
||||
description = "ASGI specs, helper code, and adapters"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
|
||||
|
||||
[[package]]
|
||||
name = "crispy-bootstrap5"
|
||||
version = "0.7"
|
||||
description = "Bootstrap5 template pack for django-crispy-forms"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
django = ">=3.2"
|
||||
django-crispy-forms = ">=1.13.0"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest", "pytest-django"]
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "4.2.5"
|
||||
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
|
||||
[package.dependencies]
|
||||
asgiref = ">=3.6.0,<4"
|
||||
sqlparse = ">=0.3.1"
|
||||
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.extras]
|
||||
argon2 = ["argon2-cffi (>=19.1.0)"]
|
||||
bcrypt = ["bcrypt"]
|
||||
|
||||
[[package]]
|
||||
name = "django-crispy-forms"
|
||||
version = "2.0"
|
||||
description = "Best way to have Django DRY forms"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
django = ">=3.2"
|
||||
|
||||
[[package]]
|
||||
name = "django-filter"
|
||||
version = "23.2"
|
||||
description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
Django = ">=3.2"
|
||||
|
||||
[[package]]
|
||||
name = "django-qr-code"
|
||||
version = "3.1.1"
|
||||
description = "An application that provides tools for displaying QR codes on your Django site."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
django = ">=3.2"
|
||||
pydantic = "*"
|
||||
pytz = "*"
|
||||
segno = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "djangorestframework"
|
||||
version = "3.14.0"
|
||||
description = "Web APIs for Django, made easy."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
django = ">=3.0"
|
||||
pytz = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "9.5.0"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
|
||||
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2"
|
||||
version = "2.9.7"
|
||||
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.3.0"
|
||||
description = "Data validation using Python type hints"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
annotated-types = ">=0.4.0"
|
||||
pydantic-core = "2.6.3"
|
||||
typing-extensions = ">=4.6.1"
|
||||
|
||||
[package.extras]
|
||||
email = ["email-validator (>=2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.6.3"
|
||||
description = ""
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2023.3"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "segno"
|
||||
version = "1.5.2"
|
||||
description = "QR Code and Micro QR Code generator for Python 2 and Python 3"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.4.4"
|
||||
description = "A non-validating SQL parser."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.extras]
|
||||
dev = ["flake8", "build"]
|
||||
doc = ["sphinx"]
|
||||
test = ["pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.7.1"
|
||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2023.3"
|
||||
description = "Provider of IANA time zone data"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2"
|
||||
|
||||
[[package]]
|
||||
name = "uwsgi"
|
||||
version = "2.0.22"
|
||||
description = "The uWSGI server"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "6e065bbdff5da7659973fa67491d6093c83b8e57d4e8dde6968ee846d382bf65"
|
||||
|
||||
[metadata.files]
|
||||
annotated-types = []
|
||||
asgiref = []
|
||||
crispy-bootstrap5 = []
|
||||
django = []
|
||||
django-crispy-forms = []
|
||||
django-filter = []
|
||||
django-qr-code = []
|
||||
djangorestframework = []
|
||||
pillow = []
|
||||
psycopg2 = []
|
||||
pydantic = []
|
||||
pydantic-core = []
|
||||
pytz = []
|
||||
segno = []
|
||||
sqlparse = []
|
||||
typing-extensions = []
|
||||
tzdata = []
|
||||
uwsgi = []
|
@ -1,23 +0,0 @@
|
||||
[tool.poetry]
|
||||
name = "shimatta-kenkyusho"
|
||||
version = "0.1.0"
|
||||
description = "Shimatta Lab Inventory System"
|
||||
authors = ["Mario Hüttel <mario.huettel@gmx.net>"]
|
||||
license = "GPLv2"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
Django = "~4.2.5"
|
||||
django-qr-code = "^3.1.1"
|
||||
djangorestframework = "^3.14.0"
|
||||
django-filter = "^23.2"
|
||||
psycopg2 = "^2.9.7"
|
||||
Pillow = "^9.3.0"
|
||||
django-crispy-forms = "^2.0"
|
||||
crispy-bootstrap5 = "^0.7"
|
||||
uwsgi = "^2.0.22"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
39
requirements.txt
Normal file
39
requirements.txt
Normal file
@ -0,0 +1,39 @@
|
||||
annotated-types==0.7.0
|
||||
asgiref==3.8.1
|
||||
astroid==3.3.5
|
||||
certifi==2024.8.30
|
||||
charset-normalizer==3.4.0
|
||||
crispy-bootstrap5==2024.10
|
||||
dill==0.3.9
|
||||
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-tex==1.1.10
|
||||
djangorestframework==3.15.2
|
||||
gunicorn==21.2.0
|
||||
idna==3.10
|
||||
isort==5.9.3
|
||||
Jinja2==3.0.1
|
||||
lazy-object-proxy==1.6.0
|
||||
MarkupSafe==2.0.1
|
||||
mccabe==0.6.1
|
||||
packaging==24.2
|
||||
Pillow==8.3.1
|
||||
pipdeptree==2.23.4
|
||||
platformdirs==4.3.6
|
||||
psycopg2-binary==2.9.9
|
||||
pydantic==2.9.2
|
||||
pydantic_core==2.23.4
|
||||
pylint==3.3.1
|
||||
requests==2.32.3
|
||||
segno==1.6.1
|
||||
setuptools==75.3.0
|
||||
sqlparse==0.4.1
|
||||
toml==0.10.2
|
||||
tomlkit==0.13.2
|
||||
typing_extensions==4.12.2
|
||||
tzdata==2024.2
|
||||
urllib3==2.2.3
|
||||
wrapt==1.12.1
|
@ -1,11 +1,12 @@
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import serializers
|
||||
from parts import models as parts_models
|
||||
|
||||
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'email', 'first_name', 'last_name', 'groups']
|
||||
model = get_user_model()
|
||||
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'groups']
|
||||
|
||||
class GroupSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
@ -23,33 +24,73 @@ class PackageSerializer(serializers.HyperlinkedModelSerializer):
|
||||
model = parts_models.Package
|
||||
fields = '__all__'
|
||||
|
||||
class StorageSerializer(serializers.HyperlinkedModelSerializer):
|
||||
full_path = serializers.ReadOnlyField(source='get_full_path')
|
||||
|
||||
class ComponentParameterSerializer(serializers.HyperlinkedModelSerializer):
|
||||
id = serializers.ReadOnlyField()
|
||||
ro_parameter_type = serializers.ReadOnlyField(source='parameter_type.parameter_name')
|
||||
class Meta:
|
||||
model = parts_models.Storage
|
||||
fields = ['url', 'id', 'name', 'parent_storage', 'responsible', 'full_path']
|
||||
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', 'ro_manufacturer_name', 'ro_image']
|
||||
|
||||
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):
|
||||
full_path = serializers.ReadOnlyField(source='get_full_path')
|
||||
|
||||
class Meta:
|
||||
model = parts_models.Storage
|
||||
fields = ['url', 'id', 'name', 'verbose_name', 'parent_storage', 'responsible', 'template', 'full_path', 'full_path_verbose']
|
||||
|
||||
class StorageSerializerStocksExpanded(StorageSerializer):
|
||||
ro_stocks = StockSerializerExpandComponent(many=True, read_only=True, source='stock_set')
|
||||
|
||||
class Meta(StorageSerializer.Meta):
|
||||
fields = StorageSerializer.Meta.fields + ['ro_stocks']
|
||||
|
||||
class DistributorSerializer(serializers.HyperlinkedModelSerializer):
|
||||
id = serializers.ReadOnlyField()
|
||||
class Meta:
|
||||
|
@ -6,7 +6,10 @@ router = routers.DefaultRouter()
|
||||
router.register(r'users', UserViewSet)
|
||||
router.register(r'groups', GroupViewSet)
|
||||
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/component-parameters', PartsComponentParameterViewSet)
|
||||
router.register(r'parts/component-distributor-numbers', PartsComponentDistributorNumViewSet)
|
||||
router.register(r'parts/stocks', PartsStockViewSet)
|
||||
router.register(r'parts/packages', PartsPackageViewSet)
|
||||
router.register(r'parts/distributors', PartsDistributorviewSet)
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework import permissions
|
||||
@ -25,7 +26,7 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
API endpoint that allows users to be viewed or edited.
|
||||
"""
|
||||
queryset = User.objects.all()
|
||||
queryset = get_user_model().objects.all()
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [filters.SearchFilter]
|
||||
@ -41,11 +42,24 @@ class GroupViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
class PartsStorageViewSet(viewsets.ModelViewSet):
|
||||
queryset = parts_models.Storage.objects.all()
|
||||
serializer_class = StorageSerializer
|
||||
permission_classes = [permissions.DjangoModelPermissions]
|
||||
filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
|
||||
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
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [filters.SearchFilter]
|
||||
search_fields = ['id', 'name']
|
||||
filterset_fields = ['id', 'name']
|
||||
|
||||
class PartsComponentViewSet(viewsets.ModelViewSet):
|
||||
queryset = parts_models.Component.objects.all()
|
||||
serializer_class = ComponentSerializer
|
||||
@ -54,6 +68,22 @@ class PartsComponentViewSet(viewsets.ModelViewSet):
|
||||
search_fields = ['id', 'name', 'package__name', 'manufacturer__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):
|
||||
queryset = parts_models.ComponentType.objects.all()
|
||||
serializer_class = ComponentTypeSerializer
|
||||
|
@ -1,23 +1,27 @@
|
||||
from django import forms
|
||||
from django.forms import widgets
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms import widgets
|
||||
from parts import models as parts_models
|
||||
from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter
|
||||
import uuid
|
||||
from django.urls import reverse
|
||||
from .qr_parser import QrCodeValidator
|
||||
|
||||
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout, Fieldset, Row, Column
|
||||
from crispy_forms.layout import Layout, Row, Column
|
||||
|
||||
class AutoCompleteWidget(widgets.Input):
|
||||
template_name = 'widgets/autocomplete-foreign-key.html'
|
||||
|
||||
def __init__(self, api_search_url, image_field_name, foreign_model, name_field_name, *args, **kwargs):
|
||||
def __init__(self, api_search_url, image_field_name, foreign_model, name_field_name, prepend=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.image_field_name = image_field_name
|
||||
self.foreign_model = foreign_model
|
||||
self.api_search_url = api_search_url
|
||||
self.name_field_name = name_field_name
|
||||
self.prepend = prepend
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
@ -47,14 +51,25 @@ class AutoCompleteWidget(widgets.Input):
|
||||
'current_instance': instance,
|
||||
'image': image,
|
||||
'name_field_name': self.name_field_name,
|
||||
'prepend': self.prepend,
|
||||
'name': display_name,
|
||||
}
|
||||
return context
|
||||
|
||||
class AutocompleteForeingKeyField(forms.UUIDField):
|
||||
def __init__(self, foreign_model=None, api_search_url=None, image_field_name='image', name_field_name='name', **kwargs):
|
||||
def __init__(self,
|
||||
foreign_model=None,
|
||||
api_search_url=None,
|
||||
image_field_name='image',
|
||||
name_field_name='name',
|
||||
prepend=None,
|
||||
**kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.widget = AutoCompleteWidget(api_search_url, image_field_name, foreign_model, name_field_name)
|
||||
self.widget = AutoCompleteWidget(api_search_url,
|
||||
image_field_name,
|
||||
foreign_model,
|
||||
name_field_name,
|
||||
prepend)
|
||||
self.foreign_model = foreign_model
|
||||
|
||||
|
||||
@ -74,21 +89,32 @@ class AutocompleteForeingKeyField(forms.UUIDField):
|
||||
raise ValidationError('Given element does not exist')
|
||||
return obj
|
||||
|
||||
|
||||
class MyTestForm(forms.Form):
|
||||
pass
|
||||
|
||||
class AddSubStorageForm(forms.Form):
|
||||
storage_name = forms.CharField(label="storage_name", initial='')
|
||||
responsible = forms.CharField(label='responsible_user')
|
||||
class ChangeStorageForm(forms.Form):
|
||||
storage_name = forms.CharField(label="Name", initial='')
|
||||
verbose_name = forms.CharField(label="Verbose Name", initial='', required=False)
|
||||
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):
|
||||
stock_uuid = forms.UUIDField()
|
||||
|
||||
class EditWatermarkForm(forms.Form):
|
||||
class EditStockBaseForm(forms.Form):
|
||||
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):
|
||||
cleaned_data = super().clean()
|
||||
@ -106,6 +132,10 @@ class EditWatermarkForm(forms.Form):
|
||||
|
||||
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):
|
||||
stock = self.cleaned_data['stock']
|
||||
active = self.cleaned_data['watermark_active']
|
||||
@ -117,26 +147,30 @@ class EditWatermarkForm(forms.Form):
|
||||
stock.watermark = watermark
|
||||
stock.save()
|
||||
|
||||
class EditStockAmountForm(forms.Form):
|
||||
stock_uuid = forms.UUIDField()
|
||||
class EditLotForm(EditStockBaseForm):
|
||||
lot = forms.IntegerField(min_value=0)
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
stock = self.cleaned_data['stock']
|
||||
amount = self.cleaned_data['amount']
|
||||
@ -188,6 +222,9 @@ class ComponentForm(forms.ModelForm):
|
||||
model = parts_models.Component
|
||||
fields = '__all__'
|
||||
|
||||
class ImportComponentForm(forms.Form):
|
||||
csv_file = forms.FileField(label="CSV File")
|
||||
|
||||
class PackageForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = parts_models.Package
|
||||
@ -222,15 +259,19 @@ class DistributorNumberDeleteForm(forms.Form):
|
||||
|
||||
class ComponentParameterDeleteForm(forms.Form):
|
||||
param_num = forms.UUIDField(required=True)
|
||||
model = parts_models.ComponentParameter
|
||||
|
||||
def clean_param_num(self):
|
||||
my_uuid = self.cleaned_data['param_num']
|
||||
try:
|
||||
param = parts_models.ComponentParameter.objects.get(id=my_uuid)
|
||||
param = self.model.objects.get(id=my_uuid)
|
||||
except:
|
||||
raise ValidationError('Parameter Number Invalid')
|
||||
return param
|
||||
|
||||
class PackageParameterDeleteForm(ComponentParameterDeleteForm):
|
||||
model = parts_models.PackageParameter
|
||||
|
||||
class AdvancedComponentSearchForm(forms.Form):
|
||||
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)
|
||||
@ -244,7 +285,6 @@ class AdvancedComponentSearchForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = False
|
||||
self.helper.disable_csrf = True
|
||||
self.helper.layout = Layout(
|
||||
Row(
|
||||
Column('name'),
|
||||
@ -263,56 +303,24 @@ class AdvancedComponentSearchForm(forms.Form):
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
PARAMETER_COMPARISON_TYPES = (
|
||||
('eq', '=='),
|
||||
('lte', '<='),
|
||||
('gte', '>='),
|
||||
)
|
||||
|
||||
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')
|
||||
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):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = False
|
||||
self.helper.disable_csrf = True
|
||||
self.helper.layout = Layout(
|
||||
Row(
|
||||
Column('parameter'),
|
||||
Column('compare_method'),
|
||||
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):
|
||||
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)
|
||||
model = parts_models.ComponentParameter
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
@ -342,4 +350,26 @@ class ComponentParameterCreateForm(forms.Form):
|
||||
else:
|
||||
text_value = ''
|
||||
value = self.cleaned_data['number_value']
|
||||
parts_models.ComponentParameter.objects.create(parameter_type=param_type, component=component, value=value, text_value=text_value)
|
||||
self.model.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])
|
@ -0,0 +1,43 @@
|
||||
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}"')
|
@ -0,0 +1,23 @@
|
||||
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')
|
@ -1,44 +0,0 @@
|
||||
# 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')},
|
||||
},
|
||||
),
|
||||
]
|
@ -0,0 +1,24 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
@ -1,19 +0,0 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
49
shimatta_kenkyusho/parts/migrations/0013_packageparameter.py
Normal file
49
shimatta_kenkyusho/parts/migrations/0013_packageparameter.py
Normal file
@ -0,0 +1,49 @@
|
||||
# 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")},
|
||||
},
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
@ -0,0 +1,23 @@
|
||||
# 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_*'."),
|
||||
),
|
||||
]
|
@ -1,11 +1,12 @@
|
||||
from django.db import models
|
||||
from shimatta_modules import RandomFileName
|
||||
from django.db.models import F, Sum
|
||||
from django.contrib.auth.models import User as AuthUser
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.dispatch import receiver
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.template import engines
|
||||
import os
|
||||
import uuid
|
||||
from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter as NumConv
|
||||
@ -29,6 +30,7 @@ class ComponentParameterType(models.Model):
|
||||
parameter_name = models.CharField(max_length=50, unique=True)
|
||||
parameter_description = models.TextField(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')
|
||||
|
||||
def __str__(self):
|
||||
@ -47,35 +49,14 @@ class ComponentType(models.Model):
|
||||
class Meta:
|
||||
ordering = ['class_name']
|
||||
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()
|
||||
possible_parameter = models.ManyToManyField(ComponentParameterType, blank=True)
|
||||
key_parameter1 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param1")
|
||||
key_parameter2 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param2")
|
||||
key_parameter3 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param3")
|
||||
description_template = models.TextField(blank=True,
|
||||
help_text="Template to assemble the dynamic description. "
|
||||
"Use template syntax, access the component with 'object', parameters with 'param_*'.")
|
||||
|
||||
def __str__(self):
|
||||
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
|
||||
return '[' + self.class_name + ']'
|
||||
|
||||
class Storage(models.Model):
|
||||
class Meta:
|
||||
@ -86,28 +67,53 @@ class Storage(models.Model):
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
|
||||
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)
|
||||
responsible = models.ForeignKey(AuthUser, on_delete=models.SET_NULL, blank=True, null=True)
|
||||
responsible = models.ForeignKey(get_user_model(), on_delete=models.SET_NULL, blank=True, null=True)
|
||||
expand_sub_storage_stocks = models.BooleanField(default=False)
|
||||
|
||||
def get_path_components(self):
|
||||
# allow storages to be templates which can be selected when adding new storages
|
||||
is_template = models.BooleanField(default=False)
|
||||
template = models.ForeignKey('self',
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name='template_of')
|
||||
|
||||
# caching variable for subtrees
|
||||
storage_list = None
|
||||
|
||||
def get_path_components(self, top_level=None):
|
||||
chain = []
|
||||
iterator = self
|
||||
chain.append(self)
|
||||
while iterator.parent_storage is not None:
|
||||
|
||||
if top_level and iterator.parent_storage == top_level:
|
||||
break
|
||||
|
||||
chain.append(iterator.parent_storage)
|
||||
iterator = iterator.parent_storage
|
||||
|
||||
return chain
|
||||
|
||||
def get_full_path(self):
|
||||
def get_full_path(self, top_level=None):
|
||||
output = ''
|
||||
|
||||
chain = self.get_path_components()
|
||||
chain = self.get_path_components(top_level)
|
||||
|
||||
for i in range(len(chain) - 1, -1, -1):
|
||||
output = output + '/' + chain[i].name
|
||||
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):
|
||||
qrdata = '[stor_uuid]' + str(self.id)
|
||||
return qrdata
|
||||
@ -121,6 +127,19 @@ class Storage(models.Model):
|
||||
else:
|
||||
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):
|
||||
if Storage.objects.exclude(id=self.id).filter(name=self.name, parent_storage__isnull=True).exists():
|
||||
if self.parent_storage is None:
|
||||
@ -131,12 +150,41 @@ class Storage(models.Model):
|
||||
raise
|
||||
|
||||
def get_total_stock_amount(self):
|
||||
stocks = Stock.objects.filter(storage=self)
|
||||
stocks = Stock.objects.filter(storage__in=self.storage_list or self.get_storage_list())
|
||||
sum = stocks.aggregate(Sum('amount'))['amount__sum']
|
||||
if sum is None:
|
||||
sum = 0
|
||||
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):
|
||||
self.validate_unique()
|
||||
super(Storage, self).save(*args, **kwargs)
|
||||
@ -168,37 +216,6 @@ class Package(models.Model):
|
||||
def __str__(self):
|
||||
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 Meta:
|
||||
@ -261,51 +278,41 @@ class Component(models.Model):
|
||||
sum = 0
|
||||
return sum
|
||||
|
||||
def get_key_parameters(self):
|
||||
"""
|
||||
Get the key parameters of a component defined by its component type.
|
||||
Returns a tuple of 3 elements. All three might be None
|
||||
"""
|
||||
p1 = None
|
||||
p2 = None
|
||||
p3 = None
|
||||
@property
|
||||
def dynamic_description(self):
|
||||
|
||||
if self.component_type:
|
||||
t = (self.component_type.key_parameter1, self.component_type.key_parameter2, self.component_type.key_parameter3)
|
||||
if t[0]:
|
||||
p1 = ComponentParameter.objects.filter(component=self, parameter_type=t[0]).first()
|
||||
if t[1]:
|
||||
p2 = ComponentParameter.objects.filter(component=self, parameter_type=t[1]).first()
|
||||
if t[2]:
|
||||
p3 = ComponentParameter.objects.filter(component=self, parameter_type=t[2]).first()
|
||||
if not self.component_type or not self.component_type.description_template:
|
||||
return ''
|
||||
|
||||
return (p1, p2, p3)
|
||||
django_engine = engines["django"]
|
||||
template = django_engine.from_string(self.component_type.description_template)
|
||||
|
||||
def get_key_parameters_as_text(self):
|
||||
params = self.get_key_parameters()
|
||||
ret_strings = []
|
||||
for p in params:
|
||||
if p:
|
||||
ret_strings.append(p.resolved_value_as_string())
|
||||
return ret_strings
|
||||
parameters = list(ComponentParameter.objects.filter(component=self))
|
||||
parameters += list(PackageParameter.objects.filter(package=self.package))
|
||||
|
||||
class ComponentParameter(models.Model):
|
||||
context = {f'param_{param.parameter_type.parameter_name}': param for param in parameters}
|
||||
context.update({'object': self})
|
||||
return template.render(context)
|
||||
|
||||
class AbstractParameter(models.Model):
|
||||
class Meta:
|
||||
unique_together = ('component', 'parameter_type')
|
||||
ordering = ['id']
|
||||
abstract = 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)
|
||||
value = models.FloatField(default=0)
|
||||
text_value = models.TextField(null=False, blank=True)
|
||||
|
||||
def _get_object_of_param(self):
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
if self.parameter_type.parameter_type == 'F':
|
||||
value = self.text_value
|
||||
else:
|
||||
value = str(self.value)
|
||||
|
||||
return str(self.component)+ ': '+ str(self.parameter_type) + ': ' + value
|
||||
return str(self._get_object_of_param())+ ': '+ str(self.parameter_type) + ': ' + value
|
||||
|
||||
def resolved_value_as_string(self):
|
||||
my_type = self.parameter_type.parameter_type
|
||||
@ -313,15 +320,50 @@ class ComponentParameter(models.Model):
|
||||
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))
|
||||
num = round(num, 3)
|
||||
return f'{num} {prefix}{self.parameter_type.unit}'
|
||||
return f'{num:.3f} {prefix}{self.parameter_type.unit}'
|
||||
elif my_type == 'N':
|
||||
# Standard float number
|
||||
num = round(self.value, 3)
|
||||
return f'{num} {self.parameter_type.unit}'
|
||||
return f'{self.value:.3f} {self.parameter_type.unit}'
|
||||
elif my_type == 'F':
|
||||
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 Meta:
|
||||
unique_together = ('component', 'storage')
|
||||
@ -341,7 +383,7 @@ class Stock(models.Model):
|
||||
return True
|
||||
|
||||
def get_qr_code(self):
|
||||
qr_data = '[stock]'+str(self.id)
|
||||
qr_data = '[stck_uuid]'+str(self.id)
|
||||
return qr_data
|
||||
|
||||
def __str__(self):
|
||||
@ -372,7 +414,7 @@ class DistributorNum(models.Model):
|
||||
|
||||
class QrPrintJob(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
|
||||
user = models.ForeignKey(AuthUser, on_delete=models.CASCADE, null=False, blank=False)
|
||||
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, null=False, blank=False)
|
||||
qrdata = models.CharField(max_length=256, 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)])
|
||||
@ -420,3 +462,26 @@ def auto_delete_file_on_change(sender, instance, **kwargs):
|
||||
return True
|
||||
if os.path.isfile(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)
|
||||
|
@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.urls import reverse as url_reverse
|
||||
import re
|
||||
|
||||
from .models import Storage, Component
|
||||
from .models import Storage, Component, Stock
|
||||
|
||||
class QrCode:
|
||||
prefix = ''
|
||||
@ -19,6 +19,7 @@ class QrCodeValidator:
|
||||
qr_patterns = {
|
||||
'stor_uuid': QrCode('stor_uuid', 'parts-stocks-detail', Storage),
|
||||
'comp_uuid': QrCode('comp_uuid', 'parts-components-detail', Component),
|
||||
'stck_uuid': QrCode('stck_uuid', 'parts-stock-detail', Stock),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
@ -32,15 +33,12 @@ class QrCodeValidator:
|
||||
|
||||
qr_type = matches.group('prefix')
|
||||
qr_uuid = matches.group('uuid')
|
||||
url_name = self.qr_patterns[qr_type].detail_view
|
||||
|
||||
model = None
|
||||
try:
|
||||
url_name = self.qr_patterns[qr_type].detail_view
|
||||
model = self.qr_patterns[qr_type].model
|
||||
except:
|
||||
model = None
|
||||
if model is None:
|
||||
raise ValidationError('QR Pattern not registered')
|
||||
except KeyError as ex:
|
||||
raise ValidationError('QR Pattern not registered') from ex
|
||||
return (model, qr_uuid, url_name)
|
||||
|
||||
def get_redirect_url(self, data):
|
||||
|
0
shimatta_kenkyusho/parts/templatetags/__init__.py
Normal file
0
shimatta_kenkyusho/parts/templatetags/__init__.py
Normal file
9
shimatta_kenkyusho/parts/templatetags/storage_tags.py
Normal file
9
shimatta_kenkyusho/parts/templatetags/storage_tags.py
Normal file
@ -0,0 +1,9 @@
|
||||
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)}'
|
@ -4,7 +4,6 @@ from . import views as parts_views
|
||||
urlpatterns = [
|
||||
path('', parts_views.MainView.as_view(), name='parts-main'),
|
||||
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('distributors/', parts_views.DistributorView.as_view(), name='parts-distributors'),
|
||||
path('stocks/', parts_views.StockView.as_view(), name='parts-stocks'),
|
||||
@ -15,7 +14,7 @@ urlpatterns = [
|
||||
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('distributors/<slug:uuid>/', parts_views.DistributorDetailView.as_view(), name='parts-distributors-detail'),
|
||||
path('manufacturers/', parts_views.ManufacturersViewSet.as_view(), name='parts-manufacturers'),
|
||||
path("manufacturers/<slug:uuid>/", parts_views.ManufacturerDetailViewSet.as_view(), name='parts-manufacturers-detail'),
|
||||
path("componenttypes/<slug:uuid>/", parts_views.ComponentTypeDetailView.as_view(), name='parts-componenttypes-detail'),
|
||||
path('manufacturers/', parts_views.ManufacturersView.as_view(), name='parts-manufacturers'),
|
||||
path("manufacturers/<slug:uuid>/", parts_views.ManufacturerDetailView.as_view(), name='parts-manufacturers-detail'),
|
||||
path("healthcheck/", parts_views.health_check_view, name='parts-health-check'),
|
||||
]
|
||||
|
File diff suppressed because it is too large
Load Diff
6
shimatta_kenkyusho/parts/views/__init__.py
Normal file
6
shimatta_kenkyusho/parts/views/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from .component_views import *
|
||||
from .distributor_views import *
|
||||
from .generic_views import *
|
||||
from .manufacturer_views import *
|
||||
from .package_views import *
|
||||
from .storage_views import *
|
110
shimatta_kenkyusho/parts/views/component_import.py
Normal file
110
shimatta_kenkyusho/parts/views/component_import.py
Normal file
@ -0,0 +1,110 @@
|
||||
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)
|
274
shimatta_kenkyusho/parts/views/component_views.py
Normal file
274
shimatta_kenkyusho/parts/views/component_views.py
Normal file
@ -0,0 +1,274 @@
|
||||
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)
|
||||
|
||||
|
120
shimatta_kenkyusho/parts/views/distributor_views.py
Normal file
120
shimatta_kenkyusho/parts/views/distributor_views.py
Normal file
@ -0,0 +1,120 @@
|
||||
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)
|
121
shimatta_kenkyusho/parts/views/generic_views.py
Normal file
121
shimatta_kenkyusho/parts/views/generic_views.py
Normal file
@ -0,0 +1,121 @@
|
||||
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)
|
121
shimatta_kenkyusho/parts/views/manufacturer_views.py
Normal file
121
shimatta_kenkyusho/parts/views/manufacturer_views.py
Normal file
@ -0,0 +1,121 @@
|
||||
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)
|
156
shimatta_kenkyusho/parts/views/package_views.py
Normal file
156
shimatta_kenkyusho/parts/views/package_views.py
Normal file
@ -0,0 +1,156 @@
|
||||
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)
|
289
shimatta_kenkyusho/parts/views/storage_views.py
Normal file
289
shimatta_kenkyusho/parts/views/storage_views.py
Normal file
@ -0,0 +1,289 @@
|
||||
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)
|
@ -8,23 +8,6 @@ https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||
|
||||
Development mode is selected by setting env variable
|
||||
- DJANGO_DEV_MODE
|
||||
|
||||
The following environment variables have to be set for Production Mode but might be optional for dev mode:
|
||||
- DJANGO_SECRET_KEY
|
||||
- DJANGO_ALLOWED_HOST
|
||||
- DJANGO_STATIC_ROOT
|
||||
- DJANGO_MEDIA_URL
|
||||
- DJANGO_MEDIA_ROOT
|
||||
- DJANGO_POSTGRESQL_SOCKET
|
||||
|
||||
|
||||
|
||||
The following can be set
|
||||
- DJANGO_SECURE_HSTS_SECONDS (defaults to 120)
|
||||
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
@ -33,34 +16,17 @@ import os
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
def get_env_value(env_variable, default=None):
|
||||
try:
|
||||
return os.environ[env_variable]
|
||||
except KeyError:
|
||||
if default is not None:
|
||||
return default
|
||||
error_msg = 'Set the {} environment variable'.format(env_variable)
|
||||
raise Exception(error_msg)
|
||||
|
||||
RUNS_IN_DEV_MODE = True if get_env_value('DJANGO_DEV_MODE', default=False) != False else False
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY=''
|
||||
if RUNS_IN_DEV_MODE:
|
||||
SECRET_KEY = get_env_value('DJANGO_SECRET_KEY',
|
||||
default='django-insecure-vq_@ue3ul@&4bz7wkcpf3pjrwf8o$7g!z-rw$ftr-$)7l3*m=^')
|
||||
else:
|
||||
SECRET_KEY = get_env_value('DJANGO_SECRET_KEY')
|
||||
SECRET_KEY = 'django-insecure-vq_@ue3ul@&4bz7wkcpf3pjrwf8o$7g!z-rw$ftr-$)7l3*m=^'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True if RUNS_IN_DEV_MODE else False
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['localhost']
|
||||
if not RUNS_IN_DEV_MODE:
|
||||
ALLOWED_HOSTS = ['localhost', get_env_value('DJANGO_ALLOWED_HOST')]
|
||||
|
||||
|
||||
# Application definition
|
||||
@ -130,36 +96,12 @@ WSGI_APPLICATION = 'shimatta_kenkyusho.wsgi.application'
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {}
|
||||
|
||||
if RUNS_IN_DEV_MODE:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
else:
|
||||
b_pw = ''
|
||||
try:
|
||||
db_pw = get_env_value('DJANGO_POSTGRESQL_PW')
|
||||
except:
|
||||
pass
|
||||
|
||||
db_user = ''
|
||||
try:
|
||||
db_user = get_env_value('DJANGO_POSTGRESQL_USER')
|
||||
except:
|
||||
pass
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'shimatta_kenkyusho',
|
||||
'USER': db_user,
|
||||
'PASSWORD': db_pw,
|
||||
'HOST': get_env_value('DJANGO_POSTGRESQL_SOCKET'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
@ -225,17 +167,12 @@ STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, "static"),
|
||||
]
|
||||
|
||||
STATIC_ROOT = None
|
||||
if not RUNS_IN_DEV_MODE:
|
||||
STATIC_ROOT = get_env_value('DJANGO_STATIC_ROOT')
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
MEDIA_URL = get_env_value('DJANGO_MEDIA_URL', default='/media/')
|
||||
MEDIA_ROOT = get_env_value('DJANGO_MEDIA_ROOT', default=os.path.join(BASE_DIR, "media"))
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
|
||||
LOGIN_URL = '/login'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
@ -246,16 +183,3 @@ SHIMATTA_KENKYUSHO_TITLE = 'しまった・研究所'
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||
|
||||
# Production only settings
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
SECURE_SSL_REDIRECT = True
|
||||
|
||||
SECURE_HSTS_SECONDS = get_env_value('DJANGO_SECURE_HSTS_SECONDS', default=120)
|
||||
|
||||
if RUNS_IN_DEV_MODE:
|
||||
SESSION_COOKIE_SECURE = False
|
||||
CSRF_COOKIE_SECURE = False
|
||||
SECURE_SSL_REDIRECT = False
|
244
shimatta_kenkyusho/shimatta_kenkyusho/settings_production.py
Normal file
244
shimatta_kenkyusho/shimatta_kenkyusho/settings_production.py
Normal file
@ -0,0 +1,244 @@
|
||||
"""
|
||||
Django settings for shimatta_kenkyusho project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.2.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||
|
||||
The following environment variables have to be set:
|
||||
- DJANGO_SECRET_KEY
|
||||
- DJANGO_ALLOWED_HOST
|
||||
- DJANGO_STATIC_ROOT
|
||||
- DJANGO_MEDIA_URL
|
||||
- DJANGO_MEDIA_ROOT
|
||||
- DJANGO_POSTGRESQL_SOCKET
|
||||
- DJANGO_POSTGRESQL_PORT
|
||||
|
||||
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_FORCE_DEV_MODE
|
||||
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
def get_env_value(env_variable, default=None):
|
||||
try:
|
||||
return os.environ[env_variable]
|
||||
except KeyError:
|
||||
if default is not None:
|
||||
return default
|
||||
error_msg = 'Set the {} environment variable'.format(env_variable)
|
||||
raise Exception(error_msg)
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = get_env_value('DJANGO_SECRET_KEY')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
if get_env_value('DJANGO_FORCE_DEV_MODE', default=False) == 'True':
|
||||
DEBUG = True
|
||||
|
||||
|
||||
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', get_env_value('DJANGO_ALLOWED_HOST')]
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'parts.apps.PartsConfig',
|
||||
'api.apps.ApiConfig',
|
||||
'rest_framework.authtoken',
|
||||
'django_filters',
|
||||
'qr_code',
|
||||
'rest_framework',
|
||||
'crispy_forms',
|
||||
'crispy_bootstrap5',
|
||||
'django.forms',
|
||||
]
|
||||
|
||||
|
||||
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'shimatta_kenkyusho.urls'
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
},
|
||||
'qr-code': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'qr-code-cache',
|
||||
'TIMEOUT': 3600
|
||||
}
|
||||
}
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(BASE_DIR, 'templates'),],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'shimatta_kenkyusho.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||
|
||||
db_pw = ''
|
||||
try:
|
||||
db_pw = get_env_value('DJANGO_POSTGRESQL_PW')
|
||||
except:
|
||||
pass
|
||||
|
||||
db_user = ''
|
||||
try:
|
||||
db_user = get_env_value('DJANGO_POSTGRESQL_USER')
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'shimatta_kenkyusho',
|
||||
'USER': db_user,
|
||||
'PASSWORD': db_pw,
|
||||
'HOST': get_env_value('DJANGO_POSTGRESQL_SOCKET'),
|
||||
'PORT': get_env_value('DJANGO_POSTGRESQL_PORT'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'api.ExpiringAuthToken.ExpiringTokenAuthentication',
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||
'PAGE_SIZE': 10,
|
||||
'DEFAULT_THROTTLE_CLASSES': [
|
||||
'rest_framework.throttling.AnonRateThrottle',
|
||||
'rest_framework.throttling.UserRateThrottle'
|
||||
],
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'anon': '100/hour',
|
||||
'user': '2000/hour'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
REST_FRAMEWORK_TOKEN_EXPIRE_HOURS = 4
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, "static"),
|
||||
]
|
||||
|
||||
STATIC_ROOT = get_env_value('DJANGO_STATIC_ROOT')
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
MEDIA_URL = get_env_value('DJANGO_MEDIA_URL')
|
||||
MEDIA_ROOT = get_env_value('DJANGO_MEDIA_ROOT')
|
||||
|
||||
LOGIN_URL = '/login'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
|
||||
|
||||
SHIMATTA_KENKYUSHO_TITLE = 'しまった・研究所'
|
||||
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||
|
||||
|
||||
# Production only settings
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
SECURE_SSL_REDIRECT = False
|
||||
|
||||
# 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)
|
@ -11,6 +11,6 @@ import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shimatta_kenkyusho.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shimatta_kenkyusho.settings_production')
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
@ -0,0 +1,2 @@
|
||||
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
|
|
14
shimatta_kenkyusho/static/example/import_csv/readme.md
Normal file
14
shimatta_kenkyusho/static/example/import_csv/readme.md
Normal file
@ -0,0 +1,14 @@
|
||||
# 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.
|
@ -12,9 +12,9 @@ function initialize_autocompletion_foreign_key_field(search_element) {
|
||||
var name_field_name = search_element.getAttribute('data-ac-name-field');
|
||||
var search_url = search_element.getAttribute('data-ac-url');
|
||||
var base_id = search_element.getAttribute('id');
|
||||
var uuid_field = search_element.parentElement.querySelector('#'+base_id+'-uuid-field');
|
||||
var dflex_container = search_element.parentElement.querySelector('#'+base_id+'-dflex-container');
|
||||
var initial_delete_button = search_element.parentElement.querySelector('[data-ac-delete]');
|
||||
var uuid_field = search_element.parentElement.parentElement.querySelector('#'+base_id+'-uuid-field');
|
||||
var dflex_container = search_element.parentElement.parentElement.querySelector('#'+base_id+'-dflex-container');
|
||||
var initial_delete_button = search_element.parentElement.parentElement.querySelector('[data-ac-delete]');
|
||||
|
||||
console.log(initial_delete_button);
|
||||
console.log(image_field_name);
|
||||
@ -27,6 +27,17 @@ 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(
|
||||
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) {
|
||||
|
@ -38,7 +38,9 @@ class AutocompleteCustomUi {
|
||||
this.query_callback = query_function.bind(this);
|
||||
|
||||
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);
|
||||
}
|
||||
}, autocomplete_query_delay_ms).bind(this));
|
||||
|
||||
this.dropdown_data = {};
|
||||
|
@ -29,6 +29,10 @@ 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);
|
||||
}
|
||||
|
||||
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) {
|
||||
return api_ajax_request_without_send('GET', api_urls_v1['component-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail);
|
||||
}
|
@ -65,6 +65,7 @@
|
||||
'user-list': '{% url 'user-list' %}',
|
||||
'groups-list': '{% url 'user-list' %}',
|
||||
'storage-list': '{% url 'storage-list' %}',
|
||||
'storage-template-list': '{% url 'storage-template-list' %}',
|
||||
'component-list': '{% url 'component-list' %}',
|
||||
'package-list': '{% url 'package-list' %}',
|
||||
'stock-list': '{% url 'stock-list' %}',
|
||||
@ -74,9 +75,29 @@
|
||||
'component-parameter-type-list': '{% url 'componentparametertype-list' %}',
|
||||
};
|
||||
</script>
|
||||
<script type="text/javascript" src="{% static 'js/kenyusho-api-v1.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/kenkyusho-api-v1.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>
|
||||
<!-- 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 %}
|
||||
{% endblock custom_scripts %}
|
||||
|
||||
|
@ -2,21 +2,21 @@
|
||||
<nav aria-label="{{aria_label}}">
|
||||
<ul class="pagination">
|
||||
{% if paginator.has_previous %}
|
||||
<li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.previous_page_number}}{% if additional_params %}&{{additional_params}}{% endif %}">«</a></li>
|
||||
<li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.previous_page_number}}">«</a></li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">«</span></li>
|
||||
{% endif %}
|
||||
{% for i in paginator.paginator.page_range %}
|
||||
{% if i <= paginator.number|add:5 and i >= paginator.number|add:-5 %}
|
||||
{% if i == paginator.number %}
|
||||
<li class="page-item active"><a class="page-link" href="?{{get_param}}={{i}}{% if additional_params %}&{{additional_params}}{% endif %}">{{i}}</a></li>
|
||||
<li class="page-item active"><a class="page-link" href="?{{get_param}}={{i}}">{{i}}</a></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="?{{get_param}}={{i}}{% if additional_params %}&{{additional_params}}{% endif %}">{{i}}</a></li>
|
||||
<li class="page-item"><a class="page-link" href="?{{get_param}}={{i}}">{{i}}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if paginator.has_next %}
|
||||
<li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.next_page_number}}{% if additional_params %}&{{additional_params}}{% endif %}">»</a></li>
|
||||
<li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.next_page_number}}">»</a></li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">»</span></li>
|
||||
{% endif %}
|
||||
|
@ -1,27 +0,0 @@
|
||||
{% 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 %}
|
@ -1,46 +0,0 @@
|
||||
{% 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"> 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 %}
|
@ -39,7 +39,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="align-middle" scope="row">
|
||||
{{component.name}}{% if key_parameter_string %}<br><span class="text-secondary">{{key_parameter_string}}</span>{% endif %}
|
||||
{{component.name}}
|
||||
</td>
|
||||
<td class="align-middle" >
|
||||
{% if component.package %}
|
||||
@ -76,6 +76,11 @@
|
||||
No description available
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if component.dynamic_description %}
|
||||
<h2>Dynamic Description</h2>
|
||||
<pre>{{ component.dynamic_description }}</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{% if component.pref_distri %}
|
||||
@ -120,6 +125,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for param in package_parameters %}
|
||||
<tr>
|
||||
<td>
|
||||
<h6 {% if param.parameter_type.parameter_description %} class="accordion-header" data-bs-toggle="collapse" data-bs-target="#collapse-pkg-parameter-desc-{{forloop.counter}}"{% endif %}>
|
||||
{{param.parameter_type.parameter_name}}
|
||||
@ -128,7 +134,8 @@
|
||||
<td>
|
||||
{{param.resolved_value_as_string}}
|
||||
</td>
|
||||
<td><span class="text-secondary">from Package</span></td>
|
||||
<td><span class="text-info">from Package</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for param in parameters %}
|
||||
<tr>
|
||||
|
@ -14,10 +14,11 @@
|
||||
</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-import-modal"><i class="bi bi-plus-circle"></i> Import CSV</button>
|
||||
</div>
|
||||
</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 %}">
|
||||
<form method="GET">
|
||||
<form method="POST">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
{% crispy advanced_search_form %}
|
||||
@ -31,9 +32,6 @@
|
||||
<input type="submit" name="submit-advanced-search" value="Search" class="btn btn-success">
|
||||
</div>
|
||||
</form>
|
||||
<template id="advanced-search-parameter-template">
|
||||
{% crispy advanced_search_param_formset.empty_form %}
|
||||
</template>
|
||||
</div>
|
||||
<div class="list-group mb-3">
|
||||
{% for comp in components %}
|
||||
@ -48,12 +46,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<h6 class="mt-0 text-primary">
|
||||
{{ comp.name }}
|
||||
{% for key_param in comp.get_key_parameters_as_text %}
|
||||
{{key_param}}
|
||||
{% endfor %}
|
||||
</h6>
|
||||
<h6 class="mt-0 text-primary">{{ comp.name }}</h6>
|
||||
{% if comp.package %}
|
||||
Package: {{comp.package}}<br>
|
||||
{% endif %}
|
||||
@ -61,7 +54,12 @@
|
||||
Manufacturer: {{comp.manufacturer}}
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="badge bg-primary rounded-pill">{{comp.get_total_amount}}</span>
|
||||
<div class="flex-grow-1 d-block ms-3" style="text-align: right;">
|
||||
<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>
|
||||
</a>
|
||||
{% endfor %}
|
||||
@ -72,6 +70,7 @@
|
||||
</div>
|
||||
|
||||
{% 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 %}
|
||||
{% block custom_scripts %}
|
||||
@ -80,5 +79,8 @@
|
||||
{% if comp_form.errors %}
|
||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('comp-edit-modal')).show()
|
||||
{% endif %}
|
||||
{% if import_comp_form.errors %}
|
||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('comp-import-modal')).show()
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock custom_scripts %}
|
@ -0,0 +1,22 @@
|
||||
{% 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>
|
@ -0,0 +1,22 @@
|
||||
{% 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>
|
@ -0,0 +1,38 @@
|
||||
{% 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>
|
@ -1,46 +0,0 @@
|
||||
{% 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>
|
@ -47,6 +47,21 @@ needs following context:
|
||||
<input type="submit" class="btn btn-primary" name="submit-edit-watermark" value="Update Watermark">
|
||||
</div>
|
||||
</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>
|
||||
|
@ -26,6 +26,46 @@
|
||||
<input type="submit" class="btn btn-primary" value="Save" name="submit-pkg-edit">
|
||||
</form>
|
||||
</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>
|
||||
|
||||
@ -80,7 +120,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'parts/modals/new-component-parameter-modal.html' with component_name=object.name form=new_param_form %}
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load qr_code %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load storage_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
@ -20,23 +21,44 @@
|
||||
<div class="row">
|
||||
{% qr_from_text object.get_qr_code size="m" image_format="svg" %}
|
||||
</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">
|
||||
{% 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> {% else %}
|
||||
<h1>Sub-Storages <a class="btn btn-secondary" href="{% url 'parts-stocks-detail' uuid=object.parent_storage.id %}">Parent Storage</a>
|
||||
{% else %}
|
||||
<h1>Sub-Storages <a class="btn btn-secondary" href="{% url 'parts-stocks'%}">Stock Overview</a>
|
||||
{% 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-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>
|
||||
<div class="list-group">
|
||||
{% for storage in storages %}
|
||||
<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">
|
||||
<div>
|
||||
<h5>{{storage.name}}</h5>
|
||||
<h5>{{storage.name}}{% if storage.verbose_name %}<small> ({{storage.verbose_name}})</small>{% endif %}</h5>
|
||||
Responsible: {{ storage.responsible }}
|
||||
</div>
|
||||
<span class="badge bg-primary rounded-pill">{{storage.get_total_stock_amount}}</span>
|
||||
<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 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>
|
||||
</a>
|
||||
{% endfor %}
|
||||
@ -69,22 +91,27 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<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>
|
||||
{% for key_param in stock.component.get_key_parameters_as_text %}
|
||||
{{key_param}}
|
||||
{% endfor %}
|
||||
</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></h6>
|
||||
{% if stock.component.package %}
|
||||
Package: {{stock.component.package}}<br>
|
||||
{% endif %}
|
||||
{% if stock.component.manufacturer %}
|
||||
Manufacturer: {{stock.component.manufacturer}}
|
||||
{% endif %}
|
||||
{% if stock.storage != storage %}
|
||||
<span class="text-secondary"><br>{{ stock.storage|get_relative_storage_path:storage }}</span>
|
||||
{% endif %}
|
||||
{% if stock.lot %}
|
||||
<span class="text-secondary"><br>Lot: {{stock.lot}}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<div class="flex-grow-2 ms-3 d-none d-lg-block" style="text-align: right;">
|
||||
<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}}
|
||||
{% if stock.watermark >= 0 %}
|
||||
<br>Watermark: {{stock.watermark}}
|
||||
@ -108,12 +135,16 @@
|
||||
{% include 'paginator.html' with paginator=stocks get_param='stock_page' aria_label='Stock Page Navigation' %}
|
||||
</div>
|
||||
</div>
|
||||
{% for stock in stocks %}
|
||||
{% include 'parts/modals/update-stock-modal.html' with stock=stock form=change_stock_form %}
|
||||
{% for stock in stocks_with_forms %}
|
||||
{% include 'parts/modals/update-stock-modal.html' with stock=stock.object form=change_stock_form relocate_form=stock.relocate_form %}
|
||||
{% endfor %}
|
||||
<!-- Modal for adding a substorage-->
|
||||
{% with add_storage_form as form %}
|
||||
{% include 'parts/modals/new-substorage-modal.html' %}
|
||||
{% include 'parts/modals/add-substorage-modal.html' %}
|
||||
{% endwith %}
|
||||
<!-- Modal to change current storage-->
|
||||
{% with change_storage_form as form %}
|
||||
{% include 'parts/modals/change-storage-modal.html' %}
|
||||
{% endwith %}
|
||||
<!-- Modal for deleting this storage -->
|
||||
{% with delete_storage_errors as err_msgs %}
|
||||
@ -162,18 +193,6 @@ api_get_component_from_id(uuid, function(component){
|
||||
}
|
||||
{% 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>
|
||||
{% endblock custom_scripts %}
|
||||
|
||||
|
@ -40,7 +40,7 @@
|
||||
<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">
|
||||
<div>
|
||||
<h5>{{storage.name}}</h5>
|
||||
<h5>{{storage.name}}{% if storage.verbose_name %}<small> ({{storage.verbose_name}})</small>{% endif %}</h5>
|
||||
Responsible: {{ storage.responsible }}
|
||||
</div>
|
||||
<span class="badge bg-primary rounded-pill">{{storage.get_total_stock_amount}}</span>
|
||||
@ -54,7 +54,7 @@
|
||||
|
||||
<!-- Add storage modal form -->
|
||||
{% with add_storage_form as form %}
|
||||
{% include 'parts/modals/new-substorage-modal.html' %}
|
||||
{% include 'parts/modals/add-substorage-modal.html' %}
|
||||
{% endwith %}
|
||||
|
||||
</div>
|
||||
@ -67,18 +67,6 @@
|
||||
var modal = bootstrap.Modal.getOrCreateInstance(addSubStorageModal);
|
||||
modal.show();
|
||||
{% 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>
|
||||
{% endblock custom_scripts %}
|
||||
|
@ -1,7 +1,12 @@
|
||||
<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}}">
|
||||
<ul id="{{widget.attrs.id}}-ac-ul" class="dropdown-menu">
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mt-3 mb-3" id="{{widget.attrs.id}}-dflex-container">
|
||||
{% if custom.current_instance %}
|
||||
{% if custom.image_field_name %}
|
||||
|
7
start_docker_compose_interactive.sh
Executable file
7
start_docker_compose_interactive.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/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
|
1
start_server.sh
Executable file
1
start_server.sh
Executable file
@ -0,0 +1 @@
|
||||
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
|
Loading…
Reference in New Issue
Block a user