From 39e40d62f4074d6dd1978e95619191351e78ddb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20H=C3=BCttel?= Date: Tue, 19 Nov 2024 21:33:12 +0100 Subject: [PATCH 1/4] Add package parameter model. Restructure Parameter models to use a common abstract base class. --- shimatta_kenkyusho/parts/admin.py | 1 + .../parts/migrations/0013_packageparameter.py | 49 +++++++++++++++++++ shimatta_kenkyusho/parts/models.py | 31 +++++++++--- 3 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 shimatta_kenkyusho/parts/migrations/0013_packageparameter.py diff --git a/shimatta_kenkyusho/parts/admin.py b/shimatta_kenkyusho/parts/admin.py index 046e01b..95de51e 100644 --- a/shimatta_kenkyusho/parts/admin.py +++ b/shimatta_kenkyusho/parts/admin.py @@ -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) diff --git a/shimatta_kenkyusho/parts/migrations/0013_packageparameter.py b/shimatta_kenkyusho/parts/migrations/0013_packageparameter.py new file mode 100644 index 0000000..f11b6a3 --- /dev/null +++ b/shimatta_kenkyusho/parts/migrations/0013_packageparameter.py @@ -0,0 +1,49 @@ +# Generated by Django 5.1.3 on 2024-11-19 20:26 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("parts", "0012_storage_verbose_name"), + ] + + operations = [ + 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")}, + }, + ), + ] diff --git a/shimatta_kenkyusho/parts/models.py b/shimatta_kenkyusho/parts/models.py index ee10485..251aed5 100644 --- a/shimatta_kenkyusho/parts/models.py +++ b/shimatta_kenkyusho/parts/models.py @@ -238,24 +238,25 @@ class Component(models.Model): sum = 0 return sum - -class ComponentParameter(models.Model): +class AbstractParameter(models.Model): class Meta: - unique_together = ('component', 'parameter_type') - ordering = ['id'] + abstract = True + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) - component = models.ForeignKey(Component, on_delete=models.CASCADE) # A target component 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 _get_object_of_param(self): + return None + def __str__(self): if self.parameter_type.parameter_type == 'F': value = self.text_value else: value = str(self.value) - return str(self.component)+ ': '+ str(self.parameter_type) + ': ' + value + return str(self._get_object_of_param())+ ': '+ str(self.parameter_type) + ': ' + value def resolved_value_as_string(self): my_type = self.parameter_type.parameter_type @@ -270,6 +271,24 @@ class ComponentParameter(models.Model): elif my_type == 'F': return self.text_value +class ComponentParameter(AbstractParameter): + class Meta: + unique_together = ('component', 'parameter_type') + ordering = ['id'] + component = models.ForeignKey(Component, on_delete=models.CASCADE) # A target component is required! + + def _get_object_of_param(self): + return self.component + +class PackageParameter(AbstractParameter): + class Meta: + unique_together = ('package', 'parameter_type') + ordering = ['id'] + package = models.ForeignKey(Package, on_delete=models.CASCADE) + + def _get_object_of_param(self): + return self.package + class Stock(models.Model): class Meta: unique_together = ('component', 'storage') -- 2.47.0 From c19f4a815984ebf94304c32c29a5838af0dd8a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20H=C3=BCttel?= Date: Sun, 24 Nov 2024 01:39:03 +0100 Subject: [PATCH 2/4] Implement package parameters --- shimatta_kenkyusho/parts/forms.py | 22 +++++++++- ...onsolidate_component_package_parameters.py | 37 ++++++++++++++++ .../parts/views/component_views.py | 4 +- .../parts/views/package_views.py | 30 ++++++++++++- .../templates/parts/components-detail.html | 20 +++++++++ .../templates/parts/packages-detail.html | 42 ++++++++++++++++++- 6 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 shimatta_kenkyusho/parts/management/commands/consolidate_component_package_parameters.py diff --git a/shimatta_kenkyusho/parts/forms.py b/shimatta_kenkyusho/parts/forms.py index 5e1743c..44752e6 100644 --- a/shimatta_kenkyusho/parts/forms.py +++ b/shimatta_kenkyusho/parts/forms.py @@ -252,15 +252,19 @@ class DistributorNumberDeleteForm(forms.Form): class ComponentParameterDeleteForm(forms.Form): param_num = forms.UUIDField(required=True) + model = parts_models.ComponentParameter def clean_param_num(self): my_uuid = self.cleaned_data['param_num'] try: - param = parts_models.ComponentParameter.objects.get(id=my_uuid) + param = self.model.objects.get(id=my_uuid) except: raise ValidationError('Parameter Number Invalid') return param +class PackageParameterDeleteForm(ComponentParameterDeleteForm): + model = parts_models.PackageParameter + class AdvancedComponentSearchForm(forms.Form): 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) @@ -309,6 +313,7 @@ class ComponentParameterSearchForm(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') value = forms.CharField(required=True, max_length=256) + model = parts_models.ComponentParameter def clean(self): data = super().clean() @@ -338,7 +343,20 @@ class ComponentParameterCreateForm(forms.Form): else: text_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): my_qr_validator = QrCodeValidator() diff --git a/shimatta_kenkyusho/parts/management/commands/consolidate_component_package_parameters.py b/shimatta_kenkyusho/parts/management/commands/consolidate_component_package_parameters.py new file mode 100644 index 0000000..e66ca5a --- /dev/null +++ b/shimatta_kenkyusho/parts/management/commands/consolidate_component_package_parameters.py @@ -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() \ No newline at end of file diff --git a/shimatta_kenkyusho/parts/views/component_views.py b/shimatta_kenkyusho/parts/views/component_views.py index 76eed61..d092c84 100644 --- a/shimatta_kenkyusho/parts/views/component_views.py +++ b/shimatta_kenkyusho/parts/views/component_views.py @@ -8,7 +8,7 @@ from django.db.models import Q from django.forms import formset_factory from django.db import IntegrityError 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 .component_import import import_components_from_csv from .generic_views import BaseTemplateMixin @@ -174,6 +174,8 @@ class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): 'distributor__name') context['parameters'] = ComponentParameter.objects.filter(component=self.object).order_by( 'parameter_type__parameter_name') + context['package_parameters'] = PackageParameter.objects.filter(package=self.object.package).order_by( + 'parameter_type__parameter_name') return context diff --git a/shimatta_kenkyusho/parts/views/package_views.py b/shimatta_kenkyusho/parts/views/package_views.py index 37ad2e7..2615581 100644 --- a/shimatta_kenkyusho/parts/views/package_views.py +++ b/shimatta_kenkyusho/parts/views/package_views.py @@ -5,7 +5,7 @@ from django.core.paginator import Paginator from django.db.models import ProtectedError from django.db.models import Q from ..forms import * -from ..models import Package +from ..models import Package, PackageParameter from .generic_views import BaseTemplateMixin class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): @@ -82,6 +82,9 @@ class PackageDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): context = super().get_context_data(**kwargs) context['package'] = 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 @@ -116,6 +119,27 @@ class PackageDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): if not edit_form.is_valid(): context['edit_form'] = edit_form 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): self.object = self.get_object() @@ -124,5 +148,9 @@ class PackageDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): return self.handle_delete_package(request) elif 'submit-pkg-edit' in request.POST: 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) \ 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 a8d2163..c565ac4 100644 --- a/shimatta_kenkyusho/templates/parts/components-detail.html +++ b/shimatta_kenkyusho/templates/parts/components-detail.html @@ -119,6 +119,19 @@ + {% for param in package_parameters %} + + +
+ {{param.parameter_type.parameter_name}} +
+ + + {{param.resolved_value_as_string}} + + from Package + + {% endfor %} {% for param in parameters %} @@ -148,6 +161,13 @@ {% endif %} {% endfor %} + {% for param in package_parameters %} + {% if param.parameter_type.parameter_description %} +
+ {{param.parameter_type.parameter_description}} +
+ {% endif %} + {% endfor %}
diff --git a/shimatta_kenkyusho/templates/parts/packages-detail.html b/shimatta_kenkyusho/templates/parts/packages-detail.html index ca00bfb..d73d461 100644 --- a/shimatta_kenkyusho/templates/parts/packages-detail.html +++ b/shimatta_kenkyusho/templates/parts/packages-detail.html @@ -26,6 +26,46 @@
+
+

Parameters

+ + + + + + + + {% for param in parameters %} + + + + + + {% endfor %} + +
ParameterValue
+
+ {{param.parameter_type.parameter_name}} +
+
+ {{param.resolved_value_as_string}} + +
+ {% csrf_token %} + + +
+
+
+ {% for param in parameters %} + {% if param.parameter_type.parameter_description %} +
+ {{param.parameter_type.parameter_description}} +
+ {% endif %} + {% endfor %} +
+
@@ -80,7 +120,7 @@ - +{% include 'parts/modals/new-component-parameter-modal.html' with component_name=object.name form=new_param_form %} {% endblock content %} -- 2.47.0 From 15b4257c7340f443dfce9cac54616260197eb0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20H=C3=BCttel?= Date: Sun, 24 Nov 2024 01:42:58 +0100 Subject: [PATCH 3/4] Add error output if inconsisten parameters are found on package and components during consolidation progress --- .../commands/consolidate_component_package_parameters.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shimatta_kenkyusho/parts/management/commands/consolidate_component_package_parameters.py b/shimatta_kenkyusho/parts/management/commands/consolidate_component_package_parameters.py index e66ca5a..09d5c46 100644 --- a/shimatta_kenkyusho/parts/management/commands/consolidate_component_package_parameters.py +++ b/shimatta_kenkyusho/parts/management/commands/consolidate_component_package_parameters.py @@ -34,4 +34,6 @@ class Command(BaseCommand): 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() \ No newline at end of file + comp_param.delete() + else: + self.stderr.write(f'\tParameter {common_type.parameter_name} is set on component {str(component)} and its package with different values: "{s1}" vs "{s2}"') \ No newline at end of file -- 2.47.0 From f2a816687451f5bf5fb5dd4089c9c35ae5e43d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20H=C3=BCttel?= Date: Sun, 24 Nov 2024 01:52:07 +0100 Subject: [PATCH 4/4] #21: Improve package parameter merging command --- .../commands/consolidate_component_package_parameters.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/shimatta_kenkyusho/parts/management/commands/consolidate_component_package_parameters.py b/shimatta_kenkyusho/parts/management/commands/consolidate_component_package_parameters.py index 09d5c46..3ab4980 100644 --- a/shimatta_kenkyusho/parts/management/commands/consolidate_component_package_parameters.py +++ b/shimatta_kenkyusho/parts/management/commands/consolidate_component_package_parameters.py @@ -19,6 +19,10 @@ class Command(BaseCommand): 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) + + # Skip trivial cases + if len(package_param_ids) == 0 or len(component_param_ids) == 0: + continue 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') @@ -36,4 +40,4 @@ class Command(BaseCommand): if not options['dry_run']: comp_param.delete() else: - self.stderr.write(f'\tParameter {common_type.parameter_name} is set on component {str(component)} and its package with different values: "{s1}" vs "{s2}"') \ No newline at end of file + self.stderr.write(f'\tParameter {common_type.parameter_name} is set on component {str(component)} and its package with different values: "{s1}" vs "{s2}"') -- 2.47.0