4 Commits

20 changed files with 130 additions and 482 deletions

1
.dockerignore Normal file
View File

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

6
Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
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"]

6
entrypoint.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/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,7 +14,6 @@ lazy-object-proxy==1.6.0
MarkupSafe==2.0.1 MarkupSafe==2.0.1
mccabe==0.6.1 mccabe==0.6.1
Pillow==8.3.1 Pillow==8.3.1
psycopg2==2.9.1
pylint==2.9.6 pylint==2.9.6
pytz==2021.1 pytz==2021.1
qrcode==7.2 qrcode==7.2
@@ -23,3 +22,5 @@ six==1.16.0
sqlparse==0.4.1 sqlparse==0.4.1
toml==0.10.2 toml==0.10.2
wrapt==1.12.1 wrapt==1.12.1
psycopg2-binary==2.9.9
gunicorn==21.2.0

View File

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

View File

@@ -244,7 +244,6 @@ class AdvancedComponentSearchForm(forms.Form):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_tag = False self.helper.form_tag = False
self.helper.disable_csrf = True
self.helper.layout = Layout( self.helper.layout = Layout(
Row( Row(
Column('name'), Column('name'),
@@ -263,53 +262,20 @@ class AdvancedComponentSearchForm(forms.Form):
), ),
) )
PARAMETER_COMPARISON_TYPES = (
('eq', '=='),
('lte', '<='),
('gte', '>='),
)
class ComponentParameterSearchForm(forms.Form): class ComponentParameterSearchForm(forms.Form):
parameter = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='parameter_name') parameter = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='parameter_name')
value = forms.CharField(max_length=100, required=False) value = forms.CharField(max_length=100, required=False)
compare_method = forms.ChoiceField(choices=PARAMETER_COMPARISON_TYPES, required=True, initial=1)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_tag = False self.helper.form_tag = False
self.helper.disable_csrf = True
self.helper.layout = Layout( self.helper.layout = Layout(
Row( Row(
Column('parameter'), Column('parameter'),
Column('compare_method'),
Column('value') Column('value')
) )
) )
def clean(self):
cleaned_data = super().clean()
parameter = cleaned_data.get('parameter')
value = cleaned_data.get('value')
if value != '' or value != None:
value = value.strip()
if value == '' or value == None:
cleaned_data['value'] = None
value = None
if parameter and value is not None and value != '':
if parameter.parameter_type != 'F':
try:
cleaned_data['value'] = EngineeringNumberConverter.engineering_to_number(value)
except:
raise ValidationError('Cannot convert value to number')
return cleaned_data
class ComponentParameterCreateForm(forms.Form): class ComponentParameterCreateForm(forms.Form):
parameter_type = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='descriptive_name') parameter_type = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='descriptive_name')
value = forms.CharField(required=True, max_length=256) value = forms.CharField(required=True, max_length=256)

View File

@@ -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')},
},
),
]

View File

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

View File

@@ -47,35 +47,11 @@ class ComponentType(models.Model):
class Meta: class Meta:
ordering = ['class_name'] ordering = ['class_name']
class_name = models.CharField(max_length=50, unique=True) class_name = models.CharField(max_length=50, unique=True)
parent_class = models.ForeignKey('self', on_delete=models.PROTECT, related_name='child_classes', null=True, blank=True)
passive = models.BooleanField() passive = models.BooleanField()
possible_parameter = models.ManyToManyField(ComponentParameterType, blank=True) possible_parameter = models.ManyToManyField(ComponentParameterType, blank=True)
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): def __str__(self):
return self.get_full_path() return '[' + self.class_name + ']'
def get_path_components(self):
chain = []
iterator = self
chain.append(self)
while iterator.parent_class is not None:
chain.append(iterator.parent_class)
iterator = iterator.parent_class
return chain
def get_full_path(self):
output = ''
chain = self.get_path_components()
for i in range(len(chain) - 1, -1, -1):
output = output + ' / ' + chain[i].class_name
return output
class Storage(models.Model): class Storage(models.Model):
class Meta: class Meta:
@@ -168,37 +144,6 @@ class Package(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class PackageParameter(models.Model):
class Meta:
unique_together = ('package', 'parameter_type')
ordering = ['id']
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
package = models.ForeignKey(Package, on_delete=models.CASCADE) # A target package is required!
parameter_type = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE)
value = models.FloatField(default=0)
text_value = models.TextField(null=False, blank=True)
def __str__(self):
if self.parameter_type.parameter_type == 'F':
value = self.text_value
else:
value = str(self.value)
return str(self.package)+ ': '+ str(self.parameter_type) + ': ' + value
def resolved_value_as_string(self):
my_type = self.parameter_type.parameter_type
if my_type == 'E' or my_type == 'I':
# Engineering float number
(num, prefix) = NumConv.number_to_engineering(self.value, it_unit=(True if my_type=='I' else False))
return f'{num:.3f} {prefix}{self.parameter_type.unit}'
elif my_type == 'N':
# Standard float number
return f'{self.value:.3f} {self.parameter_type.unit}'
elif my_type == 'F':
return self.text_value
class Manufacturer(models.Model): class Manufacturer(models.Model):
class Meta: class Meta:
@@ -261,34 +206,7 @@ class Component(models.Model):
sum = 0 sum = 0
return sum 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
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()
return (p1, p2, p3)
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
class ComponentParameter(models.Model): class ComponentParameter(models.Model):
class Meta: class Meta:
unique_together = ('component', 'parameter_type') unique_together = ('component', 'parameter_type')
@@ -313,12 +231,10 @@ class ComponentParameter(models.Model):
if my_type == 'E' or my_type == 'I': if my_type == 'E' or my_type == 'I':
# Engineering float number # Engineering float number
(num, prefix) = NumConv.number_to_engineering(self.value, it_unit=(True if my_type=='I' else False)) (num, prefix) = NumConv.number_to_engineering(self.value, it_unit=(True if my_type=='I' else False))
num = round(num, 3) return f'{num:.3f} {prefix}{self.parameter_type.unit}'
return f'{num} {prefix}{self.parameter_type.unit}'
elif my_type == 'N': elif my_type == 'N':
# Standard float number # Standard float number
num = round(self.value, 3) return f'{self.value:.3f} {self.parameter_type.unit}'
return f'{num} {self.parameter_type.unit}'
elif my_type == 'F': elif my_type == 'F':
return self.text_value return self.text_value

View File

@@ -4,7 +4,6 @@ from . import views as parts_views
urlpatterns = [ urlpatterns = [
path('', parts_views.MainView.as_view(), name='parts-main'), path('', parts_views.MainView.as_view(), name='parts-main'),
path('components/', parts_views.ComponentView.as_view(), name='parts-components'), path('components/', parts_views.ComponentView.as_view(), name='parts-components'),
path('componenttypes/', parts_views.ComponentTypeView.as_view(), name='parts-componenttypes'),
path('packages/', parts_views.PackageView.as_view(), name='parts-packages'), path('packages/', parts_views.PackageView.as_view(), name='parts-packages'),
path('distributors/', parts_views.DistributorView.as_view(), name='parts-distributors'), path('distributors/', parts_views.DistributorView.as_view(), name='parts-distributors'),
path('stocks/', parts_views.StockView.as_view(), name='parts-stocks'), path('stocks/', parts_views.StockView.as_view(), name='parts-stocks'),
@@ -17,5 +16,4 @@ urlpatterns = [
path('distributors/<slug:uuid>/', parts_views.DistributorDetailView.as_view(), name='parts-distributors-detail'), path('distributors/<slug:uuid>/', parts_views.DistributorDetailView.as_view(), name='parts-distributors-detail'),
path('manufacturers/', parts_views.ManufacturersViewSet.as_view(), name='parts-manufacturers'), path('manufacturers/', parts_views.ManufacturersViewSet.as_view(), name='parts-manufacturers'),
path("manufacturers/<slug:uuid>/", parts_views.ManufacturerDetailViewSet.as_view(), name='parts-manufacturers-detail'), 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'),
] ]

View File

@@ -2,7 +2,6 @@ from django.shortcuts import render, redirect
from django.urls import resolve, reverse from django.urls import resolve, reverse
from django.contrib.auth import logout, login from django.contrib.auth import logout, login
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.http import urlencode
from django.http import HttpResponse from django.http import HttpResponse
from .navbar import NavBar from .navbar import NavBar
from django.contrib.auth.forms import AuthenticationForm as AuthForm from django.contrib.auth.forms import AuthenticationForm as AuthForm
@@ -12,9 +11,7 @@ from django.views import View
import django.forms as forms import django.forms as forms
from django.views.generic import TemplateView, DetailView from django.views.generic import TemplateView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from .models import Storage, Stock, Component, Distributor, Manufacturer, Package from .models import Storage, Stock, Component, Distributor, Manufacturer, Package, ComponentParameter, ComponentParameterType, DistributorNum
from .models import ComponentParameter, ComponentParameterType, DistributorNum, PackageParameter
from .models import ComponentType
from .qr_parser import QrCodeValidator from .qr_parser import QrCodeValidator
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -22,12 +19,11 @@ from django.db import IntegrityError
from django.db.models import ProtectedError from django.db.models import ProtectedError
from .forms import * from .forms import *
from django.db.models import Q from django.db.models import Q
from django.db.models import Prefetch
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.forms import formset_factory from django.forms import formset_factory
import uuid import uuid
ParameterSearchFormSet = formset_factory(ComponentParameterSearchForm, extra=0) ParameterSearchFormSet = formset_factory(ComponentParameterSearchForm, extra=1)
class QrSearchForm(forms.Form): class QrSearchForm(forms.Form):
my_qr_validator = QrCodeValidator() my_qr_validator = QrCodeValidator()
@@ -38,15 +34,6 @@ class QrSearchForm(forms.Form):
qr_search = forms.CharField(label='qr_search', validators=[my_qr_validator]) qr_search = forms.CharField(label='qr_search', validators=[my_qr_validator])
class KeepSearchParamMixin(object):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
search = self.request.GET.get('search', default=None)
if search:
context['additional_params'] = urlencode({'search': search})
return context
class BaseTemplateMixin(object): class BaseTemplateMixin(object):
navbar_selected = '' navbar_selected = ''
base_title = '' base_title = ''
@@ -155,69 +142,16 @@ def login_view(request):
# Create your views here. # Create your views here.
class ComponentTypeView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/component-types.html'
base_title = 'Component Types'
default_page_size = 25
def filter_queryset(self, queryset, search_string):
if search_string is None or search_string == '':
return queryset
search_fragments = search_string.strip().split()
for search in search_fragments:
queryset = queryset.filter(Q(class_name__icontains = search))
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
search = self.request.GET.get('search', default=None)
page_num = self.request.GET.get('page', default=1)
context['search_string'] = search
queryset = ComponentType.objects.all()
types = self.filter_queryset(queryset, search)
comptypes = Paginator(types, self.default_page_size)
context['comptypes'] = comptypes.get_page(page_num)
return context
def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)
class ComponentTypeDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
model = ComponentType
base_title = ''
pk_url_kwarg = 'uuid'
template_name = 'parts/component-types-detail.html'
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 get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['breadcrumbs'] = self.get_breadcrumbs()
return context
class ComponentView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/components.html' template_name = 'parts/components.html'
base_title = 'Components' base_title = 'Components'
navbar_selected = 'Components' navbar_selected = 'Components'
default_page_size = 25 default_page_size = 25
def get_component_query_set(self, search_string): def get_component_query_set(self, search_string):
queryset = Component.objects.select_related('package', 'manufacturer', 'component_type').prefetch_related('componentparameter_set').all() queryset = Component.objects.all()
if search_string is None or search_string == '': if not search_string:
return queryset return queryset
search_fragments = search_string.strip().split() search_fragments = search_string.strip().split()
@@ -227,7 +161,7 @@ class ComponentView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin,
return queryset return queryset
def get_component_queryset_from_advanced_search(self, cleaned_data): def get_component_queryset_from_advanced_search(self, cleaned_data):
queryset = Component.objects.select_related('manufacturer', 'package', 'component_type').prefetch_related('componentparameter_set').all() queryset = Component.objects.all()
if cleaned_data['name']: if cleaned_data['name']:
queryset = queryset.filter(Q(name__icontains=cleaned_data['name'])) queryset = queryset.filter(Q(name__icontains=cleaned_data['name']))
@@ -247,21 +181,6 @@ class ComponentView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin,
queryset = queryset.filter(manufacturer=cleaned_data['manufacturer']) queryset = queryset.filter(manufacturer=cleaned_data['manufacturer'])
return queryset 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): def get_context_data_int(self, advanced_search, parameter_formset : ParameterSearchFormSet, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@@ -275,14 +194,11 @@ class ComponentView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin,
if advanced_search.is_valid(): if advanced_search.is_valid():
paginator_queryset = self.get_component_queryset_from_advanced_search(advanced_search.cleaned_data) paginator_queryset = self.get_component_queryset_from_advanced_search(advanced_search.cleaned_data)
else: else:
paginator_queryset = self.get_component_query_set(None) paginator_queryset = Component.objects.all()
# Process parameters if parameter_formset.is_valid():
for f in parameter_formset: # Process parameters
# If the form is valid and has changed compared to its initial empty state pass
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: else:
search = self.request.GET.get('search', default=None) search = self.request.GET.get('search', default=None)
@@ -296,24 +212,15 @@ class ComponentView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin,
if not parameter_formset: if not parameter_formset:
context['advanced_search_param_formset'] = ParameterSearchFormSet() context['advanced_search_param_formset'] = ParameterSearchFormSet()
if not advanced_search: if not advanced_search:
context['advanced_search_form'] = AdvancedComponentSearchForm(auto_id='adv_search_%s') context['advanced_search_form'] = AdvancedComponentSearchForm(auto_id='adv_search_%s')
return context return context
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
adv_search_form = None return self.get_context_data_int(advanced_search = None, parameter_formset=None, **kwargs)
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): def handle_new_component_post(self, request, open=False, **kwargs):
@@ -329,16 +236,33 @@ class ComponentView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin,
if open and new_component: if open and new_component:
return redirect(reverse('parts-components-detail', kwargs={'uuid':new_component.id})) return redirect(reverse('parts-components-detail', kwargs={'uuid':new_component.id}))
return self.render_to_response(context) 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): def post(self, request, *args, **kwargs):
if 'submit-edit-component' in request.POST: if 'submit-edit-component' in request.POST:
return self.handle_new_component_post(request, open=False, **kwargs) return self.handle_new_component_post(request, open=False, **kwargs)
elif 'submit-edit-component-open' in request.POST: elif 'submit-edit-component-open' in request.POST:
return self.handle_new_component_post(request, open=True, **kwargs) 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: else:
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
class PackageView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, TemplateView): class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/packages.html' template_name = 'parts/packages.html'
base_title = 'Packages' base_title = 'Packages'
navbar_selected = 'Packages' navbar_selected = 'Packages'
@@ -400,7 +324,7 @@ class PackageView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, T
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
class DistributorView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, TemplateView): class DistributorView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/distributors.html' template_name = 'parts/distributors.html'
base_title = 'Distributors' base_title = 'Distributors'
navbar_selected = 'Distributors' navbar_selected = 'Distributors'
@@ -514,7 +438,7 @@ class StockView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
return super().post(request, **kwargs) return super().post(request, **kwargs)
class StockViewDetail(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, DetailView): class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
template_name = 'parts/stocks-detail.html' template_name = 'parts/stocks-detail.html'
model = Storage model = Storage
pk_url_kwarg = 'uuid' pk_url_kwarg = 'uuid'
@@ -576,6 +500,7 @@ class StockViewDetail(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixi
context['add_storage_form'] = add_storage_form context['add_storage_form'] = add_storage_form
context['delete_storage_error'] = None context['delete_storage_error'] = None
context['add_stock_form'] = AddStockForm() context['add_stock_form'] = AddStockForm()
return context return context
def handle_add_storage_post(self, request, **kwargs): def handle_add_storage_post(self, request, **kwargs):
@@ -632,10 +557,9 @@ class StockViewDetail(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixi
if edit_form.is_valid(): if edit_form.is_valid():
edit_form.save() edit_form.save()
else: else:
pass pass # Todo: Handle error
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
return self.render_to_response(context) return self.render_to_response(context)
def handle_amount_change_post(self, request, increase, **kwargs): def handle_amount_change_post(self, request, increase, **kwargs):
@@ -686,7 +610,7 @@ class StockViewDetail(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixi
class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
template_name = 'parts/components-detail.html' template_name = 'parts/components-detail.html'
queryset = Component.objects.select_related('component_type', 'package', 'manufacturer').prefetch_related('componentparameter_set', 'distributornum_set') model = Component
pk_url_kwarg = 'uuid' pk_url_kwarg = 'uuid'
base_title = '' base_title = ''
navbar_selected = 'Components' navbar_selected = 'Components'
@@ -699,13 +623,8 @@ class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
context['comp_form'] = ComponentForm(instance=self.object) context['comp_form'] = ComponentForm(instance=self.object)
context['new_distri_num_form'] = DistributorNumberCreateForm() context['new_distri_num_form'] = DistributorNumberCreateForm()
context['new_param_form'] = ComponentParameterCreateForm() context['new_param_form'] = ComponentParameterCreateForm()
context['distri_nums'] = self.object.distributornum_set.select_related('distributor').order_by('distributor__name') context['distri_nums'] = DistributorNum.objects.filter(component=self.object).order_by('distributor__name')
context['parameters'] = self.object.componentparameter_set.order_by('parameter_type__parameter_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')
parameter_texts = self.object.get_key_parameters_as_text()
context['key_parameter_string'] = ', '.join(parameter_texts)
return context return context
@@ -915,7 +834,7 @@ class DistributorDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
class ManufacturersViewSet(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, TemplateView): class ManufacturersViewSet(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
template_name = 'parts/manufacturers.html' template_name = 'parts/manufacturers.html'
base_title = 'Manufacturers' base_title = 'Manufacturers'
navbar_selected = 'Manufacturers' navbar_selected = 'Manufacturers'

View File

@@ -138,6 +138,7 @@ DATABASES = {
'USER': db_user, 'USER': db_user,
'PASSWORD': db_pw, 'PASSWORD': db_pw,
'HOST': get_env_value('DJANGO_POSTGRESQL_SOCKET'), 'HOST': get_env_value('DJANGO_POSTGRESQL_SOCKET'),
'PORT': get_env_value('DJANGO_POSTGRESQL_PORT'),
} }
} }
@@ -229,6 +230,6 @@ CRISPY_TEMPLATE_PACK = "bootstrap5"
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True SECURE_SSL_REDIRECT = False
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

@@ -1,67 +1,67 @@
class EngineeringNumberConverter(): class EngineeringNumberConverter():
prefixes = [ prefixes = [
('y', 1e-24), ('y', 1e-24),
('z', 1e-21), ('z', 1e-21),
('a', 1e-18), ('a', 1e-18),
('f', 1e-15), ('f', 1e-15),
('p', 1e-12), ('p', 1e-12),
('n', 1e-9), ('n', 1e-9),
('u', 1e-6), ('u', 1e-6),
('m', 1e-3), ('m', 1e-3),
# We skip centi and dezi because no one really uses these besides for length measurements # We skip centi and dezi because no one really uses these besides for length measurements
('', 1), ('', 1),
# We also skip h for hekto # We also skip h for hekto
('k', 1e3), ('k', 1e3),
('M', 1e6), ('M', 1e6),
('G', 1e9), ('G', 1e9),
('T', 1e12), ('T', 1e12),
('P', 1e15), ('P', 1e15),
('E', 1e18), ('E', 1e18),
('Z', 1e21), ('Z', 1e21),
('Y', 1e24), ('Y', 1e24),
] ]
it_prefixes = [ it_prefixes = [
('', 1), ('', 1),
('Ki', 1024), ('Ki', 1024),
('Mi', 1024*1024), ('Mi', 1024*1024),
('Gi', 1024*1024*1024), ('Gi', 1024*1024*1024),
('Ti', 1024*1024*1024*1024) ('Ti', 1024*1024*1024*1024)
] ]
@classmethod
def number_to_engineering(c, number, it_unit = False):
"""
Convert a number to engineering SI syntax with prefix.
This function will return a tuple of (new_number, prefix)
"""
if it_unit:
used_prefixes = c.it_prefixes
else:
used_prefixes = c.prefixes
@classmethod if (len(used_prefixes) < 2):
def number_to_engineering(c, number, it_unit=False): return (number / used_prefixes[0][1], used_prefixes[0])
"""
Convert a number to engineering SI syntax with prefix. for i, (prefix, scale) in enumerate(used_prefixes[1:], 1):
This function will return a tuple of (new_number, prefix) if number < scale:
""" return (number / used_prefixes[i-1][1], used_prefixes[i-1][0])
if it_unit:
used_prefixes = c.it_prefixes return (number / used_prefixes[-1][1], used_prefixes[-1][0])
else:
used_prefixes = c.prefixes
if (len(used_prefixes) < 2): @classmethod
return (number / used_prefixes[0][1], used_prefixes[0]) def engineering_to_number(c, input):
cleaned_input = input.strip().replace(' ', '')
for i, (prefix, scale) in enumerate(used_prefixes[1:], 1): selected_scaling = 1
if number < scale:
return (number / used_prefixes[i-1][1], used_prefixes[i-1][0])
return (number / used_prefixes[-1][1], used_prefixes[-1][0]) for (prefix, scale) in c.prefixes+c.it_prefixes:
if prefix == '':
@classmethod continue
def engineering_to_number(c, input): if cleaned_input.endswith(prefix):
cleaned_input = input.strip().replace(' ', '') cleaned_input = cleaned_input.replace(prefix, '')
selected_scaling = scale
selected_scaling = 1 break
for (prefix, scale) in c.prefixes+c.it_prefixes: return float(cleaned_input) * selected_scaling
if prefix == '':
continue
if cleaned_input.endswith(prefix):
cleaned_input = cleaned_input.replace(prefix, '')
selected_scaling = scale
break
return float(cleaned_input) * selected_scaling

View File

@@ -2,21 +2,21 @@
<nav aria-label="{{aria_label}}"> <nav aria-label="{{aria_label}}">
<ul class="pagination"> <ul class="pagination">
{% if paginator.has_previous %} {% if paginator.has_previous %}
<li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.previous_page_number}}{% if additional_params %}&{{additional_params}}{% endif %}">&laquo;</a></li> <li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.previous_page_number}}">&laquo;</a></li>
{% else %} {% else %}
<li class="page-item disabled"><span class="page-link">&laquo;</span></li> <li class="page-item disabled"><span class="page-link">&laquo;</span></li>
{% endif %} {% endif %}
{% for i in paginator.paginator.page_range %} {% for i in paginator.paginator.page_range %}
{% if i <= paginator.number|add:5 and i >= paginator.number|add:-5 %} {% if i <= paginator.number|add:5 and i >= paginator.number|add:-5 %}
{% if i == paginator.number %} {% if i == paginator.number %}
<li class="page-item active"><a class="page-link" href="?{{get_param}}={{i}}{% 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 %} {% 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 %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if paginator.has_next %} {% if paginator.has_next %}
<li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.next_page_number}}{% if additional_params %}&{{additional_params}}{% endif %}">&raquo;</a></li> <li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.next_page_number}}">&raquo;</a></li>
{% else %} {% else %}
<li class="page-item disabled"><span class="page-link">&raquo;</span></li> <li class="page-item disabled"><span class="page-link">&raquo;</span></li>
{% endif %} {% endif %}

View File

@@ -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 %}

View File

@@ -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">&nbsp;passive</span>
</div>
{% endif %}
</li>
</a>
{% endfor %}
</div>
{% include 'paginator.html' with paginator=comptypes get_param='page' aria_label='Component Type Page Navigation' %}
</div>
</div>
</div>
{% endblock content %}
{% block custom_scripts %}
<script type="text/javascript">
</script>
{% endblock custom_scripts %}

View File

@@ -39,7 +39,7 @@
<tbody> <tbody>
<tr> <tr>
<td class="align-middle" scope="row"> <td class="align-middle" scope="row">
{{component.name}}{% if key_parameter_string %}<br><span class="text-secondary">{{key_parameter_string}}</span>{% endif %} {{component.name}}
</td> </td>
<td class="align-middle" > <td class="align-middle" >
{% if component.package %} {% if component.package %}
@@ -119,17 +119,6 @@
<th scope="col"></th> <th scope="col"></th>
</thead> </thead>
<tbody> <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-secondary">from Package</span></td>
{% endfor %}
{% for param in parameters %} {% for param in parameters %}
<tr> <tr>
<td> <td>
@@ -159,13 +148,6 @@
</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

@@ -17,7 +17,7 @@
</div> </div>
</form> </form>
<div class="collapse mb-3{% if advanced_search_shown %} show{% endif %}" id="advanced-search-collapse" aria-expanded="{% if advanced_search_shown %}true{% else %}false{% endif %}"> <div class="collapse mb-3{% if advanced_search_shown %} show{% endif %}" id="advanced-search-collapse" aria-expanded="{% if advanced_search_shown %}true{% else %}false{% endif %}">
<form method="GET"> <form method="POST">
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
{% crispy advanced_search_form %} {% crispy advanced_search_form %}
@@ -31,9 +31,6 @@
<input type="submit" name="submit-advanced-search" value="Search" class="btn btn-success"> <input type="submit" name="submit-advanced-search" value="Search" class="btn btn-success">
</div> </div>
</form> </form>
<template id="advanced-search-parameter-template">
{% crispy advanced_search_param_formset.empty_form %}
</template>
</div> </div>
<div class="list-group mb-3"> <div class="list-group mb-3">
{% for comp in components %} {% for comp in components %}
@@ -48,12 +45,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="flex-grow-1 ms-3"> <div class="flex-grow-1 ms-3">
<h6 class="mt-0 text-primary"> <h6 class="mt-0 text-primary">{{ comp.name }}</h6>
{{ comp.name }}
{% for key_param in comp.get_key_parameters_as_text %}
{{key_param}}
{% endfor %}
</h6>
{% if comp.package %} {% if comp.package %}
Package: {{comp.package}}<br> Package: {{comp.package}}<br>
{% endif %} {% endif %}

View File

@@ -69,11 +69,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="flex-grow-1 ms-3"> <div class="flex-grow-1 ms-3">
<h6 class="mt-0 text-primary"><a href="{% url 'parts-components-detail' uuid=stock.component.id %}" class="text-decoration-none">{{ stock.component.name }}</a> <h6 class="mt-0 text-primary"><a href="{% url 'parts-components-detail' uuid=stock.component.id %}" class="text-decoration-none">{{ stock.component.name }}</a></h6>
{% for key_param in stock.component.get_key_parameters_as_text %}
{{key_param}}&nbsp;
{% endfor %}
</h6>
{% if stock.component.package %} {% if stock.component.package %}
Package: {{stock.component.package}}<br> Package: {{stock.component.package}}<br>
{% endif %} {% endif %}

1
start_server.sh Executable file
View 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