Compare commits
5 commits
52b2eb2529
...
0f12c3fa74
Author | SHA1 | Date | |
---|---|---|---|
0f12c3fa74 | |||
fb92e1f9c1 | |||
f99d53c824 | |||
6197abe7d7 | |||
084d35fdf4 |
17 changed files with 460 additions and 32 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -2,9 +2,12 @@
|
||||||
__pycache__
|
__pycache__
|
||||||
.python_version
|
.python_version
|
||||||
*.egg-info
|
*.egg-info
|
||||||
*.mo
|
|
||||||
|
|
||||||
media/
|
media/
|
||||||
./static/
|
/static/
|
||||||
media.tar.gz
|
media.tar.gz
|
||||||
*.pg
|
*.pg
|
||||||
|
|
||||||
|
override.env
|
||||||
|
.env
|
21
Dockerfile
Normal file
21
Dockerfile
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
FROM python:3.13-alpine
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir poetry
|
||||||
|
|
||||||
|
ENV INVENTORY_STATIC_FILES=/static INVENTORY_MEDIA_FILES=/media
|
||||||
|
RUN mkdir -p "$INVENTORY_MEDIA_FILES" && mkdir -p "$INVENTORY_STATIC_FILES"
|
||||||
|
|
||||||
|
COPY pyproject.toml /usr/src/app
|
||||||
|
COPY poetry.lock /usr/src/app
|
||||||
|
|
||||||
|
RUN poetry install --no-root --no-interaction --no-cache
|
||||||
|
COPY manage.py ./manage.py
|
||||||
|
COPY inventory ./inventory
|
||||||
|
COPY inventory_project ./inventory_project
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
RUN poetry run python manage.py collectstatic
|
||||||
|
|
||||||
|
CMD [ "/bin/sh", "/entrypoint.sh" ]
|
95
Readme.md
95
Readme.md
|
@ -9,17 +9,19 @@ what area of your workshop you have to search for to find the part you
|
||||||
currently need. It has been optimized to store information for electronics
|
currently need. It has been optimized to store information for electronics
|
||||||
parts and small other hardware like screws, nuts and bolts.
|
parts and small other hardware like screws, nuts and bolts.
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites for manual install or docker Standalone
|
||||||
|
|
||||||
As configured by default you will need the following:
|
As configured by default you will need the following:
|
||||||
|
|
||||||
- A postgres database named `inventory` with a postgres user `inventory` that
|
- A postgres database named `inventory` with a postgres user `inventory` that
|
||||||
may connect without password or by default with the password `inventory`
|
may connect without password or by default with the password `inventory`
|
||||||
|
|
||||||
|
### Installation (manual)
|
||||||
|
|
||||||
|
You will need:
|
||||||
- Python > 3.10
|
- Python > 3.10
|
||||||
- Poetry to install requirements and create a virtualenv
|
- Poetry to install requirements and create a virtualenv
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
This is a standard Django 5.1 application, if you know how to deploy those the
|
This is a standard Django 5.1 application, if you know how to deploy those the
|
||||||
following might sound familiar:
|
following might sound familiar:
|
||||||
|
|
||||||
|
@ -27,16 +29,28 @@ following might sound familiar:
|
||||||
- Github `git clone https://github.com/dunkelstern/inventory.git`
|
- Github `git clone https://github.com/dunkelstern/inventory.git`
|
||||||
- ForgeJo: `git clone https://git.dunkelstern.de/dunkelstern/inventory.git`
|
- ForgeJo: `git clone https://git.dunkelstern.de/dunkelstern/inventory.git`
|
||||||
2. Change to checkout: `cd inventory`
|
2. Change to checkout: `cd inventory`
|
||||||
3. Install virtualenv and dependencies: `poetry install --no-root`
|
3. Install virtualenv and dependencies:
|
||||||
|
```
|
||||||
|
poetry install --no-root
|
||||||
|
```
|
||||||
4. If you want to use the system in another language than the default english set it
|
4. If you want to use the system in another language than the default english set it
|
||||||
up in the `inventory_project/settings.py`:
|
up in the `inventory_project/settings.py`:
|
||||||
```python
|
```python
|
||||||
LANGUAGE_CODE = 'en-us' # or something like 'de-de'
|
LANGUAGE_CODE = 'en-us' # or something like 'de-de'
|
||||||
```
|
```
|
||||||
see the settings file for defined languages.
|
see the settings file for defined languages.
|
||||||
5. If you changed the language rebuild the translation files: `poetry run python manage.py compilemessages`
|
5. If you changed the language rebuild the translation files:
|
||||||
6. Migrate the Database: `poetry run python manage.py migrate`
|
```
|
||||||
7. Create an admin user: `poetry run python manage.py createsuperuser`
|
poetry run python manage.py compilemessages
|
||||||
|
```
|
||||||
|
6. Migrate the Database:
|
||||||
|
```
|
||||||
|
poetry run python manage.py migrate
|
||||||
|
```
|
||||||
|
7. Optionally create an admin user. If not done manually the application will prompt you on first run.
|
||||||
|
```
|
||||||
|
poetry run python manage.py createsuperuser
|
||||||
|
```
|
||||||
8. Run the server
|
8. Run the server
|
||||||
- Development server (not for deployment!): `poetry run python manage.py runserver`
|
- Development server (not for deployment!): `poetry run python manage.py runserver`
|
||||||
- Deployment via `gunicorn` on port 8000: `poetry run gunicorn inventory_project.wsgi -b 0.0.0.0:8000`
|
- Deployment via `gunicorn` on port 8000: `poetry run gunicorn inventory_project.wsgi -b 0.0.0.0:8000`
|
||||||
|
@ -44,6 +58,73 @@ following might sound familiar:
|
||||||
Then login on `http://localhost:8000/admin/` for the Django admin interface or
|
Then login on `http://localhost:8000/admin/` for the Django admin interface or
|
||||||
go to `http://localhost:8000` to enter the inventory management system directly
|
go to `http://localhost:8000` to enter the inventory management system directly
|
||||||
|
|
||||||
|
### Installation (Standalone Docker)
|
||||||
|
|
||||||
|
#### Building yourself
|
||||||
|
|
||||||
|
1. Checkout repository:
|
||||||
|
- Github `git clone https://github.com/dunkelstern/inventory.git`
|
||||||
|
- ForgeJo: `git clone https://git.dunkelstern.de/dunkelstern/inventory.git`
|
||||||
|
2. Change to checkout: `cd inventory`
|
||||||
|
3. Build Docker image: `docker build -t 'dunkelstern/inventory:latest' .`
|
||||||
|
|
||||||
|
next steps below
|
||||||
|
|
||||||
|
#### Pulling from docker hub
|
||||||
|
|
||||||
|
1. Pull Docker image: `docker pull 'dunkelstern/inventory:latest'`
|
||||||
|
|
||||||
|
next steps below
|
||||||
|
|
||||||
|
#### Next steps
|
||||||
|
|
||||||
|
1. Install a PostgreSQL DB somewhere and create a user and DB.
|
||||||
|
2. Setup environment (put everything in a `.env` file):
|
||||||
|
```
|
||||||
|
INVENTORY_DB_HOST=
|
||||||
|
INVENTORY_DB_NAME=
|
||||||
|
INVENTORY_DB_USER=
|
||||||
|
INVENTORY_DB_PASSWORD=
|
||||||
|
|
||||||
|
INVENTORY_SECRET_KEY=
|
||||||
|
INVENTORY_EXTERNAL_URL=http://localhost:8000
|
||||||
|
INVENTORY_DEBUG=FALSE
|
||||||
|
|
||||||
|
INVENTORY_LANGUAGE=en-us
|
||||||
|
INVENTORY_TIMEZONE=UTC
|
||||||
|
|
||||||
|
INVENTORY_PAGE_SIZE=25
|
||||||
|
```
|
||||||
|
3. Create a media directory for uploaded files: `mkdir -p media`
|
||||||
|
4. Run the container:
|
||||||
|
```
|
||||||
|
docker run \
|
||||||
|
--name inventory \
|
||||||
|
-d \
|
||||||
|
--restart=always \
|
||||||
|
--env-file=.env \
|
||||||
|
-p 8000:8000 \
|
||||||
|
--volume ./media:/media \
|
||||||
|
dunkelstern/inventory:latest
|
||||||
|
```
|
||||||
|
5. The onboarding process will start on first call of the application and prompt to create an admin user.
|
||||||
|
|
||||||
|
### Installation (Docker compose)
|
||||||
|
|
||||||
|
1. Checkout repository:
|
||||||
|
- Github `git clone https://github.com/dunkelstern/inventory.git`
|
||||||
|
- ForgeJo: `git clone https://git.dunkelstern.de/dunkelstern/inventory.git`
|
||||||
|
2. Change to checkout: `cd inventory`
|
||||||
|
3. Copy `default.env` to `override.env` and check settings. Use a long random string for `INVENTORY_SECRET_KEY`!
|
||||||
|
4. Build the stack: `docker-compose up --build -d`
|
||||||
|
5. You can reach the application on port 8000
|
||||||
|
6. The onboarding process will start on first call of the application and prompt to create an admin user.
|
||||||
|
|
||||||
|
The compose stack will create two volumes:
|
||||||
|
|
||||||
|
- `inventory_dbdata` which contains the PostgreSQL database directory
|
||||||
|
- `inventory_mediafiles` which will contain any uploaded file
|
||||||
|
|
||||||
### Additional information
|
### Additional information
|
||||||
|
|
||||||
1. The initial DB migration pre-populates the database with some useful defaults
|
1. The initial DB migration pre-populates the database with some useful defaults
|
||||||
|
|
17
default.env
Normal file
17
default.env
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# override this with a long random string (used for CSRF protection)
|
||||||
|
INVENTORY_SECRET_KEY=""
|
||||||
|
|
||||||
|
# override with URL the service will be available under
|
||||||
|
INVENTORY_EXTERNAL_URL="https://inventory.example.com"
|
||||||
|
|
||||||
|
# keep this to FALSE for deployments
|
||||||
|
INVENTORY_DEBUG="FALSE"
|
||||||
|
|
||||||
|
# if you want to run the service in another language, override this
|
||||||
|
INVENTORY_LANGUAGE="en-us"
|
||||||
|
|
||||||
|
# if you want the service to use local time then override this
|
||||||
|
INVENTORY_TIMEZONE="UTC"
|
||||||
|
|
||||||
|
# This is the default pagination size
|
||||||
|
INVENTORY_PAGE_SIZE="25"
|
46
docker-compose.yaml
Normal file
46
docker-compose.yaml
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
name: inventory
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
restart: always
|
||||||
|
shm_size: 128mb
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: inventory
|
||||||
|
POSTGRES_USER: inventory
|
||||||
|
POSTGRES_DB: inventory
|
||||||
|
volumes:
|
||||||
|
- dbdata:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
inventory:
|
||||||
|
image: dunkelstern/inventory
|
||||||
|
build: .
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
env_file:
|
||||||
|
- path: ./default.env
|
||||||
|
required: true
|
||||||
|
- path: ./override.env
|
||||||
|
required: false
|
||||||
|
environment:
|
||||||
|
INVENTORY_DB_HOST: db
|
||||||
|
INVENTORY_DB_NAME: inventory
|
||||||
|
INVENTORY_DB_USER: inventory
|
||||||
|
INVENTORY_DB_PASSWORD: inventory
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
target: 8000
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
published: "8000"
|
||||||
|
protocol: tcp
|
||||||
|
app_protocol: http
|
||||||
|
mode: host
|
||||||
|
volumes:
|
||||||
|
- mediafiles:/media
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dbdata:
|
||||||
|
mediafiles:
|
8
entrypoint.sh
Normal file
8
entrypoint.sh
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cd /usr/src/app
|
||||||
|
|
||||||
|
# poetry run python manage.py collectstatic --noinput
|
||||||
|
poetry run python manage.py migrate --noinput
|
||||||
|
|
||||||
|
exec poetry run gunicorn inventory_project.wsgi -b 0.0.0.0:8000
|
BIN
inventory/locale/de/LC_MESSAGES/django.mo
Normal file
BIN
inventory/locale/de/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: 1.0\n"
|
"Project-Id-Version: 1.0\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-01-07 23:07+0100\n"
|
"POT-Creation-Date: 2025-01-11 03:20+0100\n"
|
||||||
"PO-Revision-Date: 2025-01-07 23:00+0100\n"
|
"PO-Revision-Date: 2025-01-07 23:00+0100\n"
|
||||||
"Last-Translator: Johannes Schriewer <hallo@dunkelstern.de>\n"
|
"Last-Translator: Johannes Schriewer <hallo@dunkelstern.de>\n"
|
||||||
"Language: German\n"
|
"Language: German\n"
|
||||||
|
@ -467,6 +467,62 @@ msgstr "Inventarverwaltung"
|
||||||
msgid "Create new manufacturer..."
|
msgid "Create new manufacturer..."
|
||||||
msgstr "Neuen Hersteller anlegen..."
|
msgstr "Neuen Hersteller anlegen..."
|
||||||
|
|
||||||
|
#: .\inventory\templates\inventory\onboarding.html:5
|
||||||
|
#: .\inventory\templates\inventory\onboarding.html:8
|
||||||
|
#: .\inventory\templates\inventory\onboarding_success.html:5
|
||||||
|
#: .\inventory\templates\inventory\onboarding_success.html:8
|
||||||
|
msgid "Inventory Setup"
|
||||||
|
msgstr "Inventarverwaltung Setup"
|
||||||
|
|
||||||
|
#: .\inventory\templates\inventory\onboarding.html:12
|
||||||
|
msgid "Welcome to the Inventory Management setup"
|
||||||
|
msgstr "Willkommen zur Einrichtung der Inventarverwaltung"
|
||||||
|
|
||||||
|
#: .\inventory\templates\inventory\onboarding.html:15
|
||||||
|
msgid ""
|
||||||
|
"\n"
|
||||||
|
" Currently no admin user is defined in the database.\n"
|
||||||
|
" To use the inventory management system you need at least one admin "
|
||||||
|
"user...\n"
|
||||||
|
" "
|
||||||
|
msgstr ""
|
||||||
|
"\n"
|
||||||
|
" Aktuell ist kein Admin Benutzer in der Datenbank angelegt.\n"
|
||||||
|
" Um die Inventarverwaltung nutzen zu können muss mindestens\n"
|
||||||
|
" ein Administrator angelegt werden...\n"
|
||||||
|
" "
|
||||||
|
|
||||||
|
#: .\inventory\templates\inventory\onboarding.html:22
|
||||||
|
msgid ""
|
||||||
|
"\n"
|
||||||
|
" Please verify that the following settings are correct and then fill "
|
||||||
|
"out the\n"
|
||||||
|
" form at the end and click\n"
|
||||||
|
" "
|
||||||
|
msgstr ""
|
||||||
|
" Bitte überprüfe die folgenden Einstellungen und klicke dann auf\n"
|
||||||
|
" "
|
||||||
|
|
||||||
|
#: .\inventory\templates\inventory\onboarding.html:26
|
||||||
|
#: .\inventory\templates\inventory\onboarding.html:38
|
||||||
|
#: .\inventory\templates\inventory\onboarding.html:51
|
||||||
|
msgid "Create user"
|
||||||
|
msgstr "Benutzer anlegen"
|
||||||
|
|
||||||
|
#: .\inventory\templates\inventory\onboarding.html:29
|
||||||
|
msgid "Current settings"
|
||||||
|
msgstr "Aktuelle Einstellungen"
|
||||||
|
|
||||||
|
#: .\inventory\templates\inventory\onboarding.html:42
|
||||||
|
msgid ""
|
||||||
|
"\n"
|
||||||
|
" Please correct the errors below.\n"
|
||||||
|
" "
|
||||||
|
msgstr ""
|
||||||
|
"\n"
|
||||||
|
" Bitte die unten angezeigten Fehler korrigieren.\n"
|
||||||
|
" "
|
||||||
|
|
||||||
#: .\inventory\templates\inventory\pagination.html:6
|
#: .\inventory\templates\inventory\pagination.html:6
|
||||||
#: .\inventory\templates\inventory\search_result.html:33
|
#: .\inventory\templates\inventory\search_result.html:33
|
||||||
msgid "First page"
|
msgid "First page"
|
||||||
|
@ -552,10 +608,22 @@ msgstr ""
|
||||||
msgid "Lost password?"
|
msgid "Lost password?"
|
||||||
msgstr "Passwort vergessen?"
|
msgstr "Passwort vergessen?"
|
||||||
|
|
||||||
#: .\inventory_project\settings.py:121
|
#: .\inventory\views\onboarding.py:14
|
||||||
|
msgid "Username"
|
||||||
|
msgstr "Benutzername"
|
||||||
|
|
||||||
|
#: .\inventory\views\onboarding.py:15
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "E-Mail"
|
||||||
|
|
||||||
|
#: .\inventory\views\onboarding.py:16
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Passwort"
|
||||||
|
|
||||||
|
#: .\inventory_project\settings.py:130
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr "Deutsch"
|
msgstr "Deutsch"
|
||||||
|
|
||||||
#: .\inventory_project\settings.py:122
|
#: .\inventory_project\settings.py:131
|
||||||
msgid "English"
|
msgid "English"
|
||||||
msgstr "Englisch"
|
msgstr "Englisch"
|
||||||
|
|
|
@ -335,4 +335,14 @@ div.warning-icon {
|
||||||
background-color: #c00000;
|
background-color: #c00000;
|
||||||
-webkit-mask: url(/static/inventory/img/warn.svg) no-repeat center;
|
-webkit-mask: url(/static/inventory/img/warn.svg) no-repeat center;
|
||||||
mask: url(/static/inventory/img/warn.svg) no-repeat center;
|
mask: url(/static/inventory/img/warn.svg) no-repeat center;
|
||||||
|
}
|
||||||
|
|
||||||
|
form label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
}
|
}
|
53
inventory/templates/inventory/onboarding.html
Normal file
53
inventory/templates/inventory/onboarding.html
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% translate 'Inventory Setup' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block header_bar %}
|
||||||
|
{% translate 'Inventory Setup' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% translate 'Welcome to the Inventory Management setup' %}</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% blocktranslate %}
|
||||||
|
Currently no admin user is defined in the database.
|
||||||
|
To use the inventory management system you need at least one admin user...
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% blocktranslate %}
|
||||||
|
Please verify that the following settings are correct and then fill out the
|
||||||
|
form at the end and click
|
||||||
|
{% endblocktranslate %}
|
||||||
|
<em>{% translate 'Create user' %}</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>{% translate 'Current settings' %}</h2>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
{% for key, value in settings.items %}
|
||||||
|
<dt>{{ key }}</dt>
|
||||||
|
<dd>{{ value }}</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h2>{% translate 'Create user' %}</h2>
|
||||||
|
|
||||||
|
{% if form.errors %}
|
||||||
|
<p>
|
||||||
|
{% blocktranslate %}
|
||||||
|
Please correct the errors below.
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form action="{% url 'onboarding' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
|
<input type="submit" value="{% translate 'Create user' %}">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
35
inventory/templates/inventory/onboarding_success.html
Normal file
35
inventory/templates/inventory/onboarding_success.html
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% translate 'Inventory Setup' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block header_bar %}
|
||||||
|
{% translate 'Inventory Setup' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Onboarding completed!</h2>
|
||||||
|
|
||||||
|
<p>Congratulations, you have successfully setup the Inventory management system</p>
|
||||||
|
|
||||||
|
<p>You may now log in with the password you just set up.</p>
|
||||||
|
|
||||||
|
<form method="post" action="{% url 'login' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>{{ form.username.label_tag }}</td>
|
||||||
|
<td>{{ form.username }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ form.password.label_tag }}</td>
|
||||||
|
<td>{{ form.password }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<input type="submit" value="login">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -5,10 +5,13 @@ from django.utils.formats import number_format
|
||||||
from inventory.models import Settings
|
from inventory.models import Settings
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
s = Settings.objects.first()
|
s = None
|
||||||
|
|
||||||
@register.filter(name='currency')
|
@register.filter(name='currency')
|
||||||
def currency(value, format):
|
def currency(value, format):
|
||||||
|
global s
|
||||||
|
if s is None:
|
||||||
|
s = Settings.objects.first()
|
||||||
value = float(value)
|
value = float(value)
|
||||||
|
|
||||||
if format == 'detail':
|
if format == 'detail':
|
||||||
|
|
|
@ -34,7 +34,8 @@ from .views import (
|
||||||
ManufacturerView,
|
ManufacturerView,
|
||||||
IndexView,
|
IndexView,
|
||||||
TagView,
|
TagView,
|
||||||
SearchView
|
SearchView,
|
||||||
|
OnboardingView
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -54,5 +55,6 @@ urlpatterns = [
|
||||||
path('tags', TagListView.as_view(), name='tag-list'),
|
path('tags', TagListView.as_view(), name='tag-list'),
|
||||||
path('tag/<int:pk>', TagView.as_view(), name='tag-detail'),
|
path('tag/<int:pk>', TagView.as_view(), name='tag-detail'),
|
||||||
path('search', SearchView.as_view(), name='search'),
|
path('search', SearchView.as_view(), name='search'),
|
||||||
|
path('onboarding', OnboardingView.as_view(), name='onboarding'),
|
||||||
path('', IndexView.as_view(), name='index')
|
path('', IndexView.as_view(), name='index')
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,6 +7,7 @@ from .workshop import WorkshopView, WorkshopListView
|
||||||
from .index import IndexView
|
from .index import IndexView
|
||||||
from .tag import TagListView, TagView
|
from .tag import TagListView, TagView
|
||||||
from .search import SearchView
|
from .search import SearchView
|
||||||
|
from .onboarding import OnboardingView
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
AreaView, AreaListView,
|
AreaView, AreaListView,
|
||||||
|
@ -17,5 +18,6 @@ __all__ = [
|
||||||
WorkshopView, WorkshopListView,
|
WorkshopView, WorkshopListView,
|
||||||
IndexView,
|
IndexView,
|
||||||
TagView, TagListView,
|
TagView, TagListView,
|
||||||
SearchView
|
SearchView,
|
||||||
|
OnboardingView
|
||||||
]
|
]
|
|
@ -3,14 +3,24 @@ from django.shortcuts import redirect
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.views import redirect_to_login
|
||||||
|
|
||||||
from inventory.models import Settings
|
from inventory.models import Settings
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(login_required, name='dispatch')
|
@method_decorator(login_required, name='post')
|
||||||
class IndexView(View):
|
class IndexView(View):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
User = get_user_model()
|
||||||
|
if User.objects.all().count() == 0:
|
||||||
|
# redirect to onboarding
|
||||||
|
return redirect(reverse('onboarding'))
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
path = request.get_full_path()
|
||||||
|
return redirect_to_login(path, reverse('login'))
|
||||||
|
# check settings for correct starred index page
|
||||||
settings = Settings.objects.first()
|
settings = Settings.objects.first()
|
||||||
if settings.default_container is not None:
|
if settings.default_container is not None:
|
||||||
return redirect(settings.default_container.url)
|
return redirect(settings.default_container.url)
|
||||||
|
|
60
inventory/views/onboarding.py
Normal file
60
inventory/views/onboarding.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.views.generic import View
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class OnboardingForm(forms.Form):
|
||||||
|
username = forms.CharField(label=_("Username"), max_length=150, required=True)
|
||||||
|
email = forms.EmailField(label=_("Email"), max_length=254, required=True)
|
||||||
|
password = forms.CharField(label=_("Password"), max_length=1024, min_length=8, widget=forms.PasswordInput(), required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class OnboardingView(View):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
User = get_user_model()
|
||||||
|
if User.objects.all().count() != 0:
|
||||||
|
# redirect to index
|
||||||
|
return redirect(reverse('index'))
|
||||||
|
|
||||||
|
return TemplateResponse(request, "inventory/onboarding.html", {
|
||||||
|
"settings": {
|
||||||
|
"SERVER_URL": settings.SERVER_URL,
|
||||||
|
"DEBUG": settings.DEBUG,
|
||||||
|
"ALLOWED_HOSTS": settings.ALLOWED_HOSTS,
|
||||||
|
"DATABASE_HOST": settings.DATABASES['default']['HOST'],
|
||||||
|
"DATABASE_NAME": settings.DATABASES['default']['NAME'],
|
||||||
|
"DATABASE_USER": settings.DATABASES['default']['USER'],
|
||||||
|
"DATABASE_PASSWORD": settings.DATABASES['default']['PASSWORD'],
|
||||||
|
"LANGUAGE_CODE": settings.LANGUAGE_CODE,
|
||||||
|
"TIME_ZONE": settings.TIME_ZONE,
|
||||||
|
"STATIC_ROOT": settings.STATIC_ROOT,
|
||||||
|
"MEDIA_ROOT": settings.MEDIA_ROOT,
|
||||||
|
"PAGE_SIZE": settings.PAGE_SIZE,
|
||||||
|
},
|
||||||
|
"form": OnboardingForm()
|
||||||
|
})
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# validate we have everything
|
||||||
|
form = OnboardingForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
# create superuser
|
||||||
|
User = get_user_model()
|
||||||
|
User.objects.create_superuser(
|
||||||
|
form.cleaned_data['username'],
|
||||||
|
form.cleaned_data['email'],
|
||||||
|
form.cleaned_data['password']
|
||||||
|
)
|
||||||
|
|
||||||
|
# show success screen
|
||||||
|
login_form = AuthenticationForm(data={"username": form.cleaned_data['username'] })
|
||||||
|
return TemplateResponse(request, "inventory/onboarding_success.html", {"form": login_form, "next": reverse('index')})
|
||||||
|
return TemplateResponse(request, "inventory/onboarding.html", {"settings": settings, "form": form})
|
|
@ -10,10 +10,12 @@ For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/3.0/ref/settings/
|
https://docs.djangoproject.com/en/3.0/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import socket
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from uuid import uuid4
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
|
@ -23,19 +25,25 @@ if sys.platform == 'win32':
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
# Externally visible URL of the server
|
# Externally visible URL of the server
|
||||||
SERVER_URL = "http://127.0.0.1:8000"
|
SERVER_URL = os.environ.get('INVENTORY_EXTERNAL_URL', "http://127.0.0.1:8000")
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = 'nqo*a(^g$8#0%&+*_7#b_7ybn-znk4#=45_(qy-lq-^v675pqk'
|
SECRET_KEY = os.environ.get('INVENTORY_SECRET_KEY', uuid4().hex)
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = (os.environ.get('INVENTORY_DEBUG', "FALSE") == "TRUE")
|
||||||
|
|
||||||
ALLOWED_HOSTS: List[str] = []
|
|
||||||
|
|
||||||
|
parsed_url = urlparse(SERVER_URL)
|
||||||
|
ALLOWED_HOSTS: list[str] = [
|
||||||
|
'.localhost',
|
||||||
|
'127.0.0.1',
|
||||||
|
'[::1]',
|
||||||
|
parsed_url.hostname,
|
||||||
|
socket.gethostbyname('localhost')
|
||||||
|
]
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
@ -89,9 +97,10 @@ ASGI_APPLICATION = 'inventory_project.asgi.application'
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'NAME': 'inventory',
|
'HOST': os.environ.get('INVENTORY_DB_HOST', 'localhost'),
|
||||||
'USER': 'inventory',
|
'NAME': os.environ.get('INVENTORY_DB_NAME', 'inventory'),
|
||||||
'PASSWORD': 'inventory'
|
'USER': os.environ.get('INVENTORY_DB_USER', 'inventory'),
|
||||||
|
'PASSWORD': os.environ.get('INVENTORY_DB_PASSWORD', 'inventory')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,9 +131,9 @@ LANGUAGES = [
|
||||||
("en", _("English")),
|
("en", _("English")),
|
||||||
]
|
]
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = os.environ.get('INVENTORY_LANGUAGE', 'en-us')
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = os.environ.get('INVENTORY_TIMEZONE', 'UTC')
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
|
@ -137,13 +146,13 @@ USE_TZ = True
|
||||||
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
STATIC_ROOT = os.environ.get('INVENTORY_STATIC_FILES', os.path.join(BASE_DIR, 'static'))
|
||||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||||
|
|
||||||
|
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = '/media/'
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
MEDIA_ROOT = os.environ.get('INVENTORY_MEDIA_FILES', os.path.join(BASE_DIR, 'media'))
|
||||||
SERVE_MEDIA_FILES = DEBUG
|
SERVE_MEDIA_FILES = True
|
||||||
|
|
||||||
# Default page size for paginated content
|
# Default page size for paginated content
|
||||||
PAGE_SIZE = 25
|
PAGE_SIZE = int(os.environ.get('INVENTORY_PAGE_SIZE', "25"))
|
||||||
|
|
Loading…
Reference in a new issue