Compare commits

...

13 Commits

Author SHA1 Message Date
c19f4a8159 Implement package parameters 2024-11-24 01:39:03 +01:00
26b001d983 Merge branch 'develop' into feature/21-add-package-params 2024-11-24 00:18:45 +01:00
f202896c92 Merge pull request 'issue/16-docker-uid-gid' (#28) from issue/16-docker-uid-gid into develop
Reviewed-on: #28
2024-11-23 21:57:20 +01:00
50cfe0a2c6 Fix filename typo in js file 2024-11-23 17:12:37 +01:00
2d718c5e3a Add new management command to create superuser if not present. Use that command in the entrypoint scripts 2024-11-23 17:08:38 +01:00
57b475cbe1 replace docker-compose with docker compose to make script corss compatible to non arch systems 2024-11-23 17:08:01 +01:00
25b592ee39 Let container run as user and set correct restart policy 2024-11-23 17:06:37 +01:00
08a5f97fd4 Add user ID and GID to example env file 2024-11-23 17:05:55 +01:00
511dacf54a Add restart policy to autostart the containers after boot 2024-11-23 15:27:02 +01:00
0c4f1f9dba Exclude run folder from docker. It is used for local debugging of containers 2024-11-23 15:25:43 +01:00
b873b1fd0f Merge pull request 'added CSRF trusted origin config, added tzdata - needed in debug mode' (#22) from sst/some-weird-stuff-with-docker into develop
Reviewed-on: #22
Reviewed-by: Mario Hüttel <mario.huettel@linux.com>
2024-11-21 00:50:08 +01:00
6e51085210 removed the trusted origin foo again - added proper detection of https 2024-11-19 23:31:56 +01:00
5163834de4 added CSRF trusted origin config, added tzdata - needed in debug mode 2024-11-19 23:09:53 +01:00
17 changed files with 199 additions and 12 deletions

View File

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

View File

@ -2,6 +2,12 @@
# Example configuration. Must be edited and copied to ".env" next to the compose.yaml # 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 # 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 DJANGO_STATIC_VOL=/path/to/static/root

View File

@ -1,6 +1,11 @@
x-op-restart-policy: &restart_policy
restart: unless-stopped
services: services:
shimatta-kenkyusho-web: shimatta-kenkyusho-web:
<<: *restart_policy
build: . build: .
user: "${DJANGO_USER_ID}:${DJANGO_USER_GID}"
volumes: volumes:
- "${DJANGO_STATIC_VOL:-./run/static}:/var/static" - "${DJANGO_STATIC_VOL:-./run/static}:/var/static"
- "${DJANGO_MEDIA_VOL:-./run/media}:/var/media" - "${DJANGO_MEDIA_VOL:-./run/media}:/var/media"
@ -30,6 +35,7 @@ services:
start_period: 30s start_period: 30s
shimatta-kenkyusho-db: shimatta-kenkyusho-db:
<<: *restart_policy
image: postgres:16.5-alpine image: postgres:16.5-alpine
environment: environment:
POSTGRES_PASSWORD: "${DJANGO_POSTGRESQL_PW:-p4ssw0rd}" POSTGRES_PASSWORD: "${DJANGO_POSTGRESQL_PW:-p4ssw0rd}"

View File

@ -3,4 +3,6 @@ source /home/shimatta/kenkyusho/.venv/bin/activate
cd /home/shimatta/kenkyusho/shimatta_kenkyusho cd /home/shimatta/kenkyusho/shimatta_kenkyusho
python manage.py migrate --settings shimatta_kenkyusho.settings_production python manage.py migrate --settings shimatta_kenkyusho.settings_production
python manage.py collectstatic --settings shimatta_kenkyusho.settings_production --noinput 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 gunicorn -w 4 --bind 0.0.0.0:8000 shimatta_kenkyusho.wsgi:application

View File

@ -2,5 +2,6 @@
source /home/shimatta/kenkyusho/.venv/bin/activate source /home/shimatta/kenkyusho/.venv/bin/activate
cd /home/shimatta/kenkyusho/shimatta_kenkyusho cd /home/shimatta/kenkyusho/shimatta_kenkyusho
python manage.py migrate --settings shimatta_kenkyusho.settings_production 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 python manage.py runserver 0.0.0.0:8000 --settings shimatta_kenkyusho.settings_production

View File

@ -31,5 +31,6 @@ setuptools==75.3.0
sqlparse==0.4.1 sqlparse==0.4.1
toml==0.10.2 toml==0.10.2
typing_extensions==4.12.2 typing_extensions==4.12.2
tzdata==2024.2
urllib3==2.2.3 urllib3==2.2.3
wrapt==1.12.1 wrapt==1.12.1

View File

@ -252,15 +252,19 @@ class DistributorNumberDeleteForm(forms.Form):
class ComponentParameterDeleteForm(forms.Form): class ComponentParameterDeleteForm(forms.Form):
param_num = forms.UUIDField(required=True) param_num = forms.UUIDField(required=True)
model = parts_models.ComponentParameter
def clean_param_num(self): def clean_param_num(self):
my_uuid = self.cleaned_data['param_num'] my_uuid = self.cleaned_data['param_num']
try: try:
param = parts_models.ComponentParameter.objects.get(id=my_uuid) param = self.model.objects.get(id=my_uuid)
except: except:
raise ValidationError('Parameter Number Invalid') raise ValidationError('Parameter Number Invalid')
return param return param
class PackageParameterDeleteForm(ComponentParameterDeleteForm):
model = parts_models.PackageParameter
class AdvancedComponentSearchForm(forms.Form): class AdvancedComponentSearchForm(forms.Form):
name = forms.CharField(max_length=255, label='Component Name', required=False) name = forms.CharField(max_length=255, label='Component Name', required=False)
package = AutocompleteForeingKeyField(required=False, api_search_url='package-list', foreign_model=parts_models.Package) package = AutocompleteForeingKeyField(required=False, api_search_url='package-list', foreign_model=parts_models.Package)
@ -309,6 +313,7 @@ class ComponentParameterSearchForm(forms.Form):
class ComponentParameterCreateForm(forms.Form): class ComponentParameterCreateForm(forms.Form):
parameter_type = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='descriptive_name') parameter_type = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='descriptive_name')
value = forms.CharField(required=True, max_length=256) value = forms.CharField(required=True, max_length=256)
model = parts_models.ComponentParameter
def clean(self): def clean(self):
data = super().clean() data = super().clean()
@ -338,7 +343,20 @@ class ComponentParameterCreateForm(forms.Form):
else: else:
text_value = '' text_value = ''
value = self.cleaned_data['number_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): class QrSearchForm(forms.Form):
my_qr_validator = QrCodeValidator() my_qr_validator = QrCodeValidator()

View File

@ -0,0 +1,37 @@
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)
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()

View File

@ -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')

View File

@ -8,7 +8,7 @@ from django.db.models import Q
from django.forms import formset_factory from django.forms import formset_factory
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import ProtectedError from django.db.models import ProtectedError
from ..models import Stock, Component, ComponentParameter, DistributorNum from ..models import Stock, Component, ComponentParameter, DistributorNum, PackageParameter
from ..forms import * from ..forms import *
from .component_import import import_components_from_csv from .component_import import import_components_from_csv
from .generic_views import BaseTemplateMixin from .generic_views import BaseTemplateMixin
@ -174,6 +174,8 @@ class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
'distributor__name') 'distributor__name')
context['parameters'] = ComponentParameter.objects.filter(component=self.object).order_by( context['parameters'] = ComponentParameter.objects.filter(component=self.object).order_by(
'parameter_type__parameter_name') 'parameter_type__parameter_name')
context['package_parameters'] = PackageParameter.objects.filter(package=self.object.package).order_by(
'parameter_type__parameter_name')
return context return context

View File

@ -5,7 +5,7 @@ from django.core.paginator import Paginator
from django.db.models import ProtectedError from django.db.models import ProtectedError
from django.db.models import Q from django.db.models import Q
from ..forms import * from ..forms import *
from ..models import Package from ..models import Package, PackageParameter
from .generic_views import BaseTemplateMixin from .generic_views import BaseTemplateMixin
class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
@ -82,6 +82,9 @@ class PackageDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['package'] = self.object context['package'] = self.object
context['edit_form'] = PackageForm(instance=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 return context
@ -117,6 +120,27 @@ class PackageDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
context['edit_form'] = edit_form context['edit_form'] = edit_form
return self.render_to_response(context) 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): def post(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
@ -124,5 +148,9 @@ class PackageDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
return self.handle_delete_package(request) return self.handle_delete_package(request)
elif 'submit-pkg-edit' in request.POST: elif 'submit-pkg-edit' in request.POST:
return self.edit_package(request) 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) return super().post(request, *args, **kwargs)

View File

@ -56,7 +56,6 @@ if get_env_value('DJANGO_FORCE_DEV_MODE', default=False) == 'True':
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', get_env_value('DJANGO_ALLOWED_HOST')] ALLOWED_HOSTS = ['127.0.0.1', 'localhost', get_env_value('DJANGO_ALLOWED_HOST')]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
@ -239,4 +238,7 @@ CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = False 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) SECURE_HSTS_SECONDS = get_env_value('DJANGO_SECURE_HSTS_SECONDS', default=120)

View File

@ -75,7 +75,7 @@
'component-parameter-type-list': '{% url 'componentparametertype-list' %}', 'component-parameter-type-list': '{% url 'componentparametertype-list' %}',
}; };
</script> </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.js' %}"></script>
<script type="text/javascript" src="{% static 'js/autocomplete-foreign-key-field.js' %}"></script> <script type="text/javascript" src="{% static 'js/autocomplete-foreign-key-field.js' %}"></script>
<!-- Initialize bootstrap popovers --> <!-- Initialize bootstrap popovers -->

View File

@ -119,6 +119,19 @@
<th scope="col"></th> <th scope="col"></th>
</thead> </thead>
<tbody> <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}}
</h6>
</td>
<td>
{{param.resolved_value_as_string}}
</td>
<td><span class="text-info">from Package</span></td>
</tr>
{% endfor %}
{% for param in parameters %} {% for param in parameters %}
<tr> <tr>
<td> <td>
@ -148,6 +161,13 @@
</div> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% for param in package_parameters %}
{% if param.parameter_type.parameter_description %}
<div class="collapse accordion-collapse" id="collapse-pkg-parameter-desc-{{forloop.counter}}" data-bs-parent="#accordion-param-desc">
{{param.parameter_type.parameter_description}}
</div>
{% endif %}
{% endfor %}
</div> </div>
</div> </div>
<div class="col"> <div class="col">

View File

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

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# Startup the db container # Startup the db container
docker-compose start shimatta-kenkyusho-db docker compose start shimatta-kenkyusho-db
# Override entrypoint to get interactive shell # Override entrypoint to get interactive shell
docker-compose run --entrypoint="/bin/sh" -p 8000:8000 shimatta-kenkyusho-web docker compose run --entrypoint="/bin/sh" -p 8000:8000 shimatta-kenkyusho-web