diff --git a/shimatta_kenkyusho/parts/forms.py b/shimatta_kenkyusho/parts/forms.py index c66d67d..59ec547 100644 --- a/shimatta_kenkyusho/parts/forms.py +++ b/shimatta_kenkyusho/parts/forms.py @@ -1,6 +1,7 @@ from django import forms from django.core.exceptions import ValidationError from parts import models as parts_models +from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter class MyTestForm(forms.Form): pass @@ -200,3 +201,63 @@ class EditComponentForm(forms.Form): self.instance.package = self.cleaned_data['package_object'] self.instance.component_type = self.cleaned_data['component_type_object'] self.instance.save() + +class EditComponentParameterForm(forms.Form): + parameter_type = forms.CharField() # This must come first. Do not change the order of these elements! + value = forms.CharField() + + def __init__(self, *args, **kwargs): + + init_values = kwargs.get('initial', None) + if init_values is not None: + if isinstance(init_values['parameter_type'], parts_models.ComponentParameterType): + type_instance = init_values['parameter_type'] + self.parameter_type_object = type_instance + kwargs['initial']['parameter_type'] = init_values['parameter_type'].parameter_name + if isinstance(init_values['value'], int) or isinstance(init_values['value'], float): + if type_instance.engineering_unit: + (num, prefix) = EngineeringNumberConverter.number_to_engineering(init_values['value'], False) + kwargs['initial']['value'] = f'{num} {prefix}' + elif type_instance.it_unit: + (num, prefix) = EngineeringNumberConverter.number_to_engineering(init_values['value'], True) + kwargs['initial']['value'] = f'{num} {prefix}' + + super().__init__(*args, **kwargs) + + def clean_parameter_type(self): + data = self.cleaned_data['parameter_type'] + + param_instance = None + try: + param_instance = parts_models.ComponentParameterType.objects.get(parameter_name=data) + except: + raise ValidationError(f'Component Parameter Type {data} is not defined') + + self.cleaned_data['parameter_type_object'] = param_instance + return data + + def clean_value(self): + parameter_type = self.cleaned_data.get('parameter_type_object', None) + value_data = self.cleaned_data['value'] + + if parameter_type is None: + raise ValidationError('Cannot convert value for unknown parameter type') + + processed_value = None + + if parameter_type.it_unit or parameter_type.engineering_unit: + try: + processed_value = EngineeringNumberConverter.engineering_to_number(value_data) + except: + raise ValidationError(f'Cannot not convert Value "{value_data}" to a number') + elif parameter_type.freetext_parameter: + processed_value = value_data + else: + try: + processed_value = float(value_data) + except: + raise ValidationError(f'"{value_data}" is not a valid number') + + self.cleaned_data['processed_value'] = processed_value + + return value_data \ No newline at end of file diff --git a/shimatta_kenkyusho/parts/migrations/0003_alter_componentparametertype_unit.py b/shimatta_kenkyusho/parts/migrations/0003_alter_componentparametertype_unit.py new file mode 100644 index 0000000..0ee7471 --- /dev/null +++ b/shimatta_kenkyusho/parts/migrations/0003_alter_componentparametertype_unit.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2021-11-12 18:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parts', '0002_alter_componenttype_possible_parameter'), + ] + + operations = [ + migrations.AlterField( + model_name='componentparametertype', + name='unit', + field=models.CharField(blank=True, max_length=10, null=True), + ), + ] diff --git a/shimatta_kenkyusho/parts/models.py b/shimatta_kenkyusho/parts/models.py index 19d1f70..89a75e6 100644 --- a/shimatta_kenkyusho/parts/models.py +++ b/shimatta_kenkyusho/parts/models.py @@ -19,13 +19,17 @@ class ComponentParameterType(models.Model): parameter_name = models.CharField(max_length=50, unique=True) parameter_description = models.TextField(null=True, blank=True) - unit = models.CharField(max_length=10) + unit = models.CharField(max_length=10, null=True, blank=True) freetext_parameter = models.BooleanField() engineering_unit = models.BooleanField() it_unit = models.BooleanField() def __str__(self): - return self.parameter_name + ' in ' + self.unit + unit = self.unit + if unit: + return self.parameter_name + ' in ' + unit + else: + return self.parameter_name class ComponentType(models.Model): class Meta: diff --git a/shimatta_kenkyusho/parts/views.py b/shimatta_kenkyusho/parts/views.py index 0db0612..3823c28 100644 --- a/shimatta_kenkyusho/parts/views.py +++ b/shimatta_kenkyusho/parts/views.py @@ -11,12 +11,12 @@ 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 +from .models import Storage, Stock, Component, Distributor, Manufacturer, Package, ComponentParameter, ComponentParameterType from .qr_parser import QrCodeValidator from django.core.paginator import Paginator from django.core.exceptions import ValidationError from django.db import IntegrityError -from .forms import MyTestForm, AddSubStorageForm, DeleteStockForm, EditWatermarkForm, EditStockAmountForm, AddStockForm, EditComponentForm +from .forms import MyTestForm, AddSubStorageForm, DeleteStockForm, EditWatermarkForm, EditStockAmountForm, AddStockForm, EditComponentForm, EditComponentParameterForm from django.db.models import Q from django.db.models.functions import Lower import uuid @@ -265,7 +265,6 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView): componente_stock_page = self.request.GET.get('stock_page', default=1) - stock_paginator = Paginator(self.search_stock_queryset(stock_search_input), self.default_pagination_size) context['storages'] = storage_paginator.get_page(storage_page) @@ -391,12 +390,28 @@ class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): pk_url_kwarg = 'uuid' base_title = '' navbar_selected = 'Components' + + def preparae_initial_param_formset_data(self): + parameters = ComponentParameter.objects.filter(component=self.object) + initdata = [] + for param in parameters: + param_type = param.parameter_type + if param_type.freetext_parameter: + value = param.text_value + else: + value = param.value + initdata.append({'parameter_type': param_type, 'value': value}) + return initdata def get_context_data(self, **kwargs): self.base_title = 'Component / '+self.object.name context = super().get_context_data(**kwargs) context['component'] = self.object context['edit_form'] = EditComponentForm(instance=self.object) + + ParameterFormset = forms.formset_factory(EditComponentParameterForm, extra=0, max_num=100) + context['param_formset'] = ParameterFormset( + initial=self.preparae_initial_param_formset_data()) return context def handle_submit_edit_post(self, request, **kwargs): @@ -420,10 +435,23 @@ class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): context['edit_form'] = form return self.render_to_response(context) + def handle_submit_edit_params_post(self, request, **kwargs): + + f = EditComponentParameterForm(data=request.POST) + if f.is_valid(): + print('Valid') + else: + print('Invalid') + + context = self.get_context_data() + return self.render_to_response(context) + def post(self, request, *args, **kwargs): self.object = self.get_object() if 'submit-edit-comp' in request.POST: return self.handle_submit_edit_post(request, **kwargs) + elif 'submit-edit-params' in request.POST: + return self.handle_submit_edit_params_post(request, **kwargs) return super().post(request, *args, **kwargs) diff --git a/shimatta_kenkyusho/shimatta_modules/EngineeringNumberConverter.py b/shimatta_kenkyusho/shimatta_modules/EngineeringNumberConverter.py new file mode 100644 index 0000000..42f7559 --- /dev/null +++ b/shimatta_kenkyusho/shimatta_modules/EngineeringNumberConverter.py @@ -0,0 +1,67 @@ + +class EngineeringNumberConverter(): + + prefixes = [ + ('y', 1e-24), + ('z', 1e-21), + ('a', 1e-18), + ('f', 1e-15), + ('p', 1e-12), + ('n', 1e-9), + ('u', 1e-6), + ('m', 1e-3), + # We skip centi and dezi because no one really uses these besides for length measurements + ('', 1), + # We also skip h for hekto + ('k', 1e3), + ('M', 1e6), + ('G', 1e9), + ('T', 1e12), + ('P', 1e15), + ('E', 1e18), + ('Z', 1e21), + ('Y', 1e24), + ] + it_prefixes = [ + ('', 1), + ('Ki', 1024), + ('Mi', 1024*1024), + ('Gi', 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 + + if (len(used_prefixes) < 2): + return (number / used_prefixes[0][1], used_prefixes[0]) + + for i, (prefix, scale) in enumerate(used_prefixes[1:], 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]) + + @classmethod + def engineering_to_number(c, input): + cleaned_input = input.strip().replace(' ', '') + + selected_scaling = 1 + + for (prefix, scale) in c.prefixes+c.it_prefixes: + if prefix == '': + continue + if cleaned_input.endswith(prefix): + cleaned_input = cleaned_input.replace(prefix, '') + selected_scaling = scale + break + + return float(cleaned_input) * selected_scaling \ No newline at end of file diff --git a/shimatta_kenkyusho/templates/parts/components-detail.html b/shimatta_kenkyusho/templates/parts/components-detail.html index 0b48d59..1f62822 100644 --- a/shimatta_kenkyusho/templates/parts/components-detail.html +++ b/shimatta_kenkyusho/templates/parts/components-detail.html @@ -1,6 +1,7 @@ {% extends 'base.html' %} {% load static %} {% load qr_code %} +{% load crispy_forms_tags %} {% block content %}