5 Commits

15 changed files with 184 additions and 117 deletions

View File

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

View File

@@ -1,6 +0,0 @@
FROM alpine:latest
RUN apk add --no-cache python3 py3-pip python3-dev py3-setuptools gcc python3-dev jpeg-dev zlib-dev musl-dev py3-gunicorn
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"]

View File

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

View File

@@ -14,6 +14,7 @@ lazy-object-proxy==1.6.0
MarkupSafe==2.0.1
mccabe==0.6.1
Pillow==8.3.1
psycopg2==2.9.1
pylint==2.9.6
pytz==2021.1
qrcode==7.2
@@ -22,5 +23,3 @@ six==1.16.0
sqlparse==0.4.1
toml==0.10.2
wrapt==1.12.1
psycopg2-binary==2.9.9
gunicorn==21.2.0

View File

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

View File

@@ -244,6 +244,7 @@ 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'),
@@ -262,20 +263,53 @@ 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)

View File

@@ -1,59 +0,0 @@
from django.core.management.base import BaseCommand
from parts.models import Component, Package, Distributor, Manufacturer
import os
import shutil
class Command(BaseCommand):
help = 'Migrate all media files to the current folder structure'
def move_files_of_model(self, queryset):
for comp in queryset:
img_path = comp.image.name
img_path = os.path.normpath(img_path)
path_components = img_path.split(os.sep)
if len(path_components) <= 2:
self.stdout.write(f'Legacy path found: {img_path}. Will be moved')
full_path_components = os.path.normpath(comp.image.path).split(os.sep)
fname = full_path_components[-1]
path_elem_count = len(full_path_components)
full_path_components.insert(path_elem_count-1, str(fname[1]))
full_path_components.insert(path_elem_count-1, str(fname[0]))
dest_path = os.sep.join(full_path_components)
# Move file
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
shutil.move(comp.image.path, dest_path)
# Update model
new_rel_path_comps = path_components
l = len(new_rel_path_comps)
new_rel_path_comps.insert(l-1, str(fname[1]))
new_rel_path_comps.insert(l-1, str(fname[0]))
new_name = os.sep.join(new_rel_path_comps)
self.stdout.write(f'New location: {dest_path}, new name: {new_name}')
comp.image.name = new_name
comp.save()
def handle(self, *args, **kwargs):
self.stdout.write('Querying components...')
components = Component.objects.exclude(image='')
self.stdout.write(f'Count of components with images: {components.count()}');
self.move_files_of_model(components)
self.stdout.write('Querying packages...')
pkgs = Package.objects.exclude(image='')
self.stdout.write(f'Count of components with images: {pkgs.count()}');
self.move_files_of_model(pkgs)
self.stdout.write('Querying manufacturers...')
manufacturers = Manufacturer.objects.exclude(image='')
self.stdout.write(f'Count of components with images: {manufacturers.count()}');
self.move_files_of_model(manufacturers)
self.stdout.write('Querying distributors...')
distris = Distributor.objects.exclude(image='')
self.stdout.write(f'Count of components with images: {distris.count()}');
self.move_files_of_model(distris)

View File

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

View File

@@ -49,7 +49,10 @@ class ComponentType(models.Model):
class_name = models.CharField(max_length=50, unique=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")
def __str__(self):
return '[' + self.class_name + ']'
@@ -144,6 +147,37 @@ 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:

View File

@@ -11,7 +11,7 @@ from django.views import View
import django.forms as forms
from django.views.generic import TemplateView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from .models import Storage, Stock, Component, Distributor, Manufacturer, Package, ComponentParameter, ComponentParameterType, DistributorNum
from .models import Storage, Stock, Component, Distributor, Manufacturer, Package, ComponentParameter, ComponentParameterType, DistributorNum, PackageParameter
from .qr_parser import QrCodeValidator
from django.core.paginator import Paginator
from django.core.exceptions import ValidationError
@@ -23,7 +23,7 @@ from django.db.models.functions import Lower
from django.forms import formset_factory
import uuid
ParameterSearchFormSet = formset_factory(ComponentParameterSearchForm, extra=1)
ParameterSearchFormSet = formset_factory(ComponentParameterSearchForm, extra=0)
class QrSearchForm(forms.Form):
my_qr_validator = QrCodeValidator()
@@ -149,9 +149,9 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
default_page_size = 25
def get_component_query_set(self, search_string):
queryset = Component.objects.all()
queryset = Component.objects.select_related('package', 'manufacturer').all()
if not search_string:
if search_string is None or search_string == '':
return queryset
search_fragments = search_string.strip().split()
@@ -161,7 +161,7 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
return queryset
def get_component_queryset_from_advanced_search(self, cleaned_data):
queryset = Component.objects.all()
queryset = Component.objects.select_related('manufacturer', 'package').all()
if cleaned_data['name']:
queryset = queryset.filter(Q(name__icontains=cleaned_data['name']))
@@ -181,6 +181,21 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
queryset = queryset.filter(manufacturer=cleaned_data['manufacturer'])
return queryset
def filter_queryset_with_parameters(self, queryset, parameter, value, compare_method):
if parameter and (value is None or value == ''):
return queryset.filter(Q(componentparameter__parameter_type=parameter))
elif parameter and value is not None:
if parameter.parameter_type == 'F':
return queryset.filter(Q(componentparameter__text_value__icontains=value) & Q(componentparameter__parameter_type=parameter))
else:
if compare_method == 'lte': # <=
return queryset.filter(Q(componentparameter__value__lte=value) & Q(componentparameter__parameter_type=parameter))
elif compare_method == 'gte': # >=
return queryset.filter(Q(componentparameter__value__gte=value) & Q(componentparameter__parameter_type=parameter))
else:
return queryset.filter(Q(componentparameter__value=value) & Q(componentparameter__parameter_type=parameter))
return queryset
def get_context_data_int(self, advanced_search, parameter_formset : ParameterSearchFormSet, **kwargs):
context = super().get_context_data(**kwargs)
@@ -194,11 +209,14 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
if advanced_search.is_valid():
paginator_queryset = self.get_component_queryset_from_advanced_search(advanced_search.cleaned_data)
else:
paginator_queryset = Component.objects.all()
paginator_queryset = self.get_component_query_set(None)
if parameter_formset.is_valid():
# Process parameters
pass
# Process parameters
for f in parameter_formset:
# If the form is valid and has changed compared to its initial empty state
if f.is_valid() and f.has_changed():
paginator_queryset = self.filter_queryset_with_parameters(paginator_queryset, f.cleaned_data['parameter'], f.cleaned_data['value'], f.cleaned_data['compare_method'])
else:
search = self.request.GET.get('search', default=None)
@@ -212,15 +230,24 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
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)
adv_search_form = None
adv_param_search_formset = None
if 'submit-advanced-search' in self.request.GET:
adv_search_form = AdvancedComponentSearchForm(auto_id='adv_search_%s', data=self.request.GET)
adv_param_search_formset = ParameterSearchFormSet(data=self.request.GET)
if adv_search_form.is_valid():
pass
if adv_param_search_formset.is_valid():
pass
return self.get_context_data_int(advanced_search = adv_search_form, parameter_formset=adv_param_search_formset, **kwargs)
def handle_new_component_post(self, request, open=False, **kwargs):
@@ -236,29 +263,12 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
if open and new_component:
return redirect(reverse('parts-components-detail', kwargs={'uuid':new_component.id}))
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-advanced-search' in request.POST:
return self.handle_advanced_search_post(request, **kwargs)
else:
return super().post(request, *args, **kwargs)
@@ -557,9 +567,10 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
if edit_form.is_valid():
edit_form.save()
else:
pass # Todo: Handle error
pass
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def handle_amount_change_post(self, request, increase, **kwargs):
@@ -625,7 +636,9 @@ class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
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')
if self.object.package:
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):

View File

@@ -138,7 +138,6 @@ DATABASES = {
'USER': db_user,
'PASSWORD': db_pw,
'HOST': get_env_value('DJANGO_POSTGRESQL_SOCKET'),
'PORT': get_env_value('DJANGO_POSTGRESQL_PORT'),
}
}
@@ -230,6 +229,6 @@ CRISPY_TEMPLATE_PACK = "bootstrap5"
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = False
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = get_env_value('DJANGO_SECURE_HSTS_SECONDS', default=120)

View File

@@ -6,13 +6,8 @@ from django.utils.deconstruct import deconstructible
@deconstructible
class RandomFileName(object):
def __init__(self, path):
self.path = os.path.join(path, "%s/%s/%s%s")
self.path = os.path.join(path, "%s%s")
def __call__(self, _, filename):
extension = os.path.splitext(filename)[1]
file_uuid = uuid.uuid4()
uuid_str = str(file_uuid)
first_char = uuid_str[0]
second_char = uuid_str[1]
return self.path % (first_char, second_char, file_uuid, extension)
return self.path % (uuid.uuid4(), extension)

View File

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

View File

@@ -17,7 +17,7 @@
</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="POST">
<form method="GET">
<div class="row">
<div class="col-sm">
{% crispy advanced_search_form %}
@@ -31,6 +31,9 @@
<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 %}

View File

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