diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index d970fb802d..1ba4f259fb 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -2,11 +2,14 @@ # InvenTree API version -INVENTREE_API_VERSION = 56 +INVENTREE_API_VERSION = 57 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v57 -> 2022-06-05 : https://github.com/inventree/InvenTree/pull/3130 + - Transfer PartCategoryTemplateParameter actions to the API + v56 -> 2022-06-02 : https://github.com/inventree/InvenTree/pull/3123 - Expose the PartParameterTemplate model to use the API diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 43ea17f060..cfac04fa03 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -194,7 +194,7 @@ class CategoryMetadata(generics.RetrieveUpdateAPIView): queryset = PartCategory.objects.all() -class CategoryParameterList(generics.ListAPIView): +class CategoryParameterList(generics.ListCreateAPIView): """API endpoint for accessing a list of PartCategoryParameterTemplate objects. - GET: Return a list of PartCategoryParameterTemplate objects @@ -235,6 +235,13 @@ class CategoryParameterList(generics.ListAPIView): return queryset +class CategoryParameterDetail(generics.RetrieveUpdateDestroyAPIView): + """Detail endpoint fro the PartCategoryParameterTemplate model""" + + queryset = PartCategoryParameterTemplate.objects.all() + serializer_class = part_serializers.CategoryParameterTemplateSerializer + + class CategoryTree(generics.ListAPIView): """API endpoint for accessing a list of PartCategory objects ready for rendering a tree.""" @@ -1855,7 +1862,11 @@ part_api_urls = [ # Base URL for PartCategory API endpoints re_path(r'^category/', include([ re_path(r'^tree/', CategoryTree.as_view(), name='api-part-category-tree'), - re_path(r'^parameters/', CategoryParameterList.as_view(), name='api-part-category-parameter-list'), + + re_path(r'^parameters/', include([ + re_path('^(?P\d+)/', CategoryParameterDetail.as_view(), name='api-part-category-parameter-detail'), + re_path('^.*$', CategoryParameterList.as_view(), name='api-part-category-parameter-list'), + ])), # Category detail endpoints re_path(r'^(?P\d+)/', include([ diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 4f17838171..fa9983cf3e 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -10,8 +10,8 @@ from InvenTree.fields import RoundingDecimalFormField from InvenTree.forms import HelperForm from InvenTree.helpers import clean_decimal -from .models import (Part, PartCategory, PartCategoryParameterTemplate, - PartInternalPriceBreak, PartSellPriceBreak) +from .models import (Part, PartCategory, PartInternalPriceBreak, + PartSellPriceBreak) class PartImageDownloadForm(HelperForm): @@ -59,29 +59,6 @@ class SetPartCategoryForm(forms.Form): part_category = TreeNodeChoiceField(queryset=PartCategory.objects.all(), required=True, help_text=_('Select part category')) -class EditCategoryParameterTemplateForm(HelperForm): - """Form for editing a PartCategoryParameterTemplate object.""" - - add_to_same_level_categories = forms.BooleanField(required=False, - initial=False, - help_text=_('Add parameter template to same level categories')) - - add_to_all_categories = forms.BooleanField(required=False, - initial=False, - help_text=_('Add parameter template to all categories')) - - class Meta: - """Metaclass defines fields for this form""" - model = PartCategoryParameterTemplate - fields = [ - 'category', - 'parameter_template', - 'default_value', - 'add_to_same_level_categories', - 'add_to_all_categories', - ] - - class PartPriceForm(forms.Form): """Simple form for viewing part pricing information.""" diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d5a6900c8c..436745eadb 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2383,7 +2383,9 @@ class PartParameter(models.Model): class PartCategoryParameterTemplate(models.Model): - """A PartCategoryParameterTemplate creates a unique relationship between a PartCategory and a PartParameterTemplate. Multiple PartParameterTemplate instances can be associated to a PartCategory to drive a default list of parameter templates attached to a Part instance upon creation. + """A PartCategoryParameterTemplate creates a unique relationship between a PartCategory and a PartParameterTemplate. + + Multiple PartParameterTemplate instances can be associated to a PartCategory to drive a default list of parameter templates attached to a Part instance upon creation. Attributes: category: Reference to a single PartCategory object diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 031260563f..57a840855e 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -753,10 +753,9 @@ class BomItemSerializer(InvenTreeModelSerializer): class CategoryParameterTemplateSerializer(InvenTreeModelSerializer): - """Serializer for PartCategoryParameterTemplate.""" + """Serializer for the PartCategoryParameterTemplate model.""" - parameter_template = PartParameterTemplateSerializer(many=False, - read_only=True) + parameter_template_detail = PartParameterTemplateSerializer(source='parameter_template', many=False, read_only=True) category_detail = CategorySerializer(source='category', many=False, read_only=True) @@ -768,6 +767,7 @@ class CategoryParameterTemplateSerializer(InvenTreeModelSerializer): 'category', 'category_detail', 'parameter_template', + 'parameter_template_detail', 'default_value', ] diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 6af000f283..0df4fefc72 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -14,6 +14,7 @@ from InvenTree.api_tester import InvenTreeAPITestCase from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus, StockStatus) from part.models import (BomItem, BomItemSubstitute, Part, PartCategory, + PartCategoryParameterTemplate, PartParameterTemplate, PartRelated) from stock.models import StockItem, StockLocation @@ -24,6 +25,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase): fixtures = [ 'category', 'part', + 'params', 'location', 'bom', 'company', @@ -40,6 +42,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase): 'part.delete', 'part_category.change', 'part_category.add', + 'part_category.delete', ] def test_category_list(self): @@ -94,6 +97,57 @@ class PartCategoryAPITest(InvenTreeAPITestCase): self.assertEqual(metadata['water'], 'melon') self.assertEqual(metadata['abc'], 'ABC') + def test_category_parameters(self): + """Test that the PartCategoryParameterTemplate API function work""" + + url = reverse('api-part-category-parameter-list') + + response = self.get(url, {}, expected_code=200) + + self.assertEqual(len(response.data), 2) + + # Add some more category templates via the API + n = PartParameterTemplate.objects.count() + + for template in PartParameterTemplate.objects.all(): + response = self.post( + url, + { + 'category': 2, + 'parameter_template': template.pk, + 'default_value': 'xyz', + } + ) + + # Total number of category templates should have increased + response = self.get(url, {}, expected_code=200) + self.assertEqual(len(response.data), 2 + n) + + # Filter by category + response = self.get( + url, + { + 'category': 2, + } + ) + + self.assertEqual(len(response.data), n) + + # Test that we can retrieve individual templates via the API + for template in PartCategoryParameterTemplate.objects.all(): + url = reverse('api-part-category-parameter-detail', kwargs={'pk': template.pk}) + + data = self.get(url, {}, expected_code=200).data + + for key in ['pk', 'category', 'category_detail', 'parameter_template', 'parameter_template_detail', 'default_value']: + self.assertIn(key, data.keys()) + + # Test that we can delete via the API also + response = self.delete(url, expected_code=204) + + # There should not be any templates left at this point + self.assertEqual(PartCategoryParameterTemplate.objects.count(), 0) + class PartOptionsAPITest(InvenTreeAPITestCase): """Tests for the various OPTIONS endpoints in the /part/ API. diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index cafad19122..9c70b364c9 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -28,12 +28,6 @@ part_detail_urls = [ re_path(r'^.*$', views.PartDetail.as_view(), name='part-detail'), ] -category_parameter_urls = [ - re_path(r'^new/', views.CategoryParameterTemplateCreate.as_view(), name='category-param-template-create'), - re_path(r'^(?P\d+)/edit/', views.CategoryParameterTemplateEdit.as_view(), name='category-param-template-edit'), - re_path(r'^(?P\d+)/delete/', views.CategoryParameterTemplateDelete.as_view(), name='category-param-template-delete'), -] - category_urls = [ # Top level subcategory display @@ -42,8 +36,6 @@ category_urls = [ # Category detail views re_path(r'(?P\d+)/', include([ re_path(r'^delete/', views.CategoryDelete.as_view(), name='category-delete'), - re_path(r'^parameters/', include(category_parameter_urls)), - # Anything else re_path(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'), ])) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 50225c8348..e2698044f6 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -9,8 +9,6 @@ from django.contrib import messages from django.core.exceptions import ValidationError from django.core.files.base import ContentFile from django.db import transaction -from django.db.utils import IntegrityError -from django.forms import HiddenInput from django.shortcuts import HttpResponseRedirect, get_object_or_404 from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -27,8 +25,8 @@ from common.models import InvenTreeSetting from common.views import FileManagementAjaxView, FileManagementFormView from company.models import SupplierPart from InvenTree.helpers import str2bool -from InvenTree.views import (AjaxCreateView, AjaxDeleteView, AjaxUpdateView, - AjaxView, InvenTreeRoleMixin, QRCodeView) +from InvenTree.views import (AjaxDeleteView, AjaxUpdateView, AjaxView, + InvenTreeRoleMixin, QRCodeView) from order.models import PurchaseOrderLineItem from plugin.views import InvenTreePluginViewMixin from stock.models import StockItem, StockLocation @@ -36,7 +34,7 @@ from stock.models import StockItem, StockLocation from . import forms as part_forms from . import settings as part_settings from .bom import ExportBom, IsValidBOMFormat, MakeBomTemplate -from .models import Part, PartCategory, PartCategoryParameterTemplate +from .models import Part, PartCategory class PartIndex(InvenTreeRoleMixin, ListView): @@ -984,185 +982,3 @@ class CategoryDelete(AjaxDeleteView): return { 'danger': _('Part category was deleted'), } - - -class CategoryParameterTemplateCreate(AjaxCreateView): - """View for creating a new PartCategoryParameterTemplate.""" - - model = PartCategoryParameterTemplate - form_class = part_forms.EditCategoryParameterTemplateForm - ajax_form_title = _('Create Category Parameter Template') - - def get_initial(self): - """Get initial data for Category.""" - initials = super().get_initial() - - category_id = self.kwargs.get('pk', None) - - if category_id: - try: - initials['category'] = PartCategory.objects.get(pk=category_id) - except (PartCategory.DoesNotExist, ValueError): - pass - - return initials - - def get_form(self): - """Create a form to upload a new CategoryParameterTemplate. - - - Hide the 'category' field (parent part) - - Display parameter templates which are not yet related - """ - form = super().get_form() - - form.fields['category'].widget = HiddenInput() - - if form.is_valid(): - form.cleaned_data['category'] = self.kwargs.get('pk', None) - - try: - # Get selected category - category = self.get_initial()['category'] - - # Get existing parameter templates - parameters = [template.parameter_template.pk - for template in category.get_parameter_templates()] - - # Exclude templates already linked to category - updated_choices = [] - for choice in form.fields["parameter_template"].choices: - if (choice[0] not in parameters): - updated_choices.append(choice) - - # Update choices for parameter templates - form.fields['parameter_template'].choices = updated_choices - except KeyError: - pass - - return form - - def post(self, request, *args, **kwargs): - """Capture the POST request. - - - If the add_to_all_categories object is set, link parameter template to - all categories - - If the add_to_same_level_categories object is set, link parameter template to - same level categories - """ - form = self.get_form() - - valid = form.is_valid() - - if valid: - add_to_same_level_categories = form.cleaned_data['add_to_same_level_categories'] - add_to_all_categories = form.cleaned_data['add_to_all_categories'] - - selected_category = PartCategory.objects.get(pk=int(self.kwargs['pk'])) - parameter_template = form.cleaned_data['parameter_template'] - default_value = form.cleaned_data['default_value'] - - categories = PartCategory.objects.all() - - if add_to_same_level_categories and not add_to_all_categories: - # Get level - level = selected_category.level - # Filter same level categories - categories = categories.filter(level=level) - - if add_to_same_level_categories or add_to_all_categories: - # Add parameter template and default value to categories - for category in categories: - # Skip selected category (will be processed in the post call) - if category.pk != selected_category.pk: - try: - cat_template = PartCategoryParameterTemplate.objects.create(category=category, - parameter_template=parameter_template, - default_value=default_value) - cat_template.save() - except IntegrityError: - # Parameter template is already linked to category - pass - - return super().post(request, *args, **kwargs) - - -class CategoryParameterTemplateEdit(AjaxUpdateView): - """View for editing a PartCategoryParameterTemplate.""" - - model = PartCategoryParameterTemplate - form_class = part_forms.EditCategoryParameterTemplateForm - ajax_form_title = _('Edit Category Parameter Template') - - def get_object(self): - """Returns the PartCategoryParameterTemplate associated with this view - - - First, attempt lookup based on supplied 'pid' kwarg - - Else, attempt lookup based on supplied 'pk' kwarg - """ - try: - self.object = self.model.objects.get(pk=self.kwargs['pid']) - except: - return None - - return self.object - - def get_form(self): - """Create a form to upload a new CategoryParameterTemplate. - - - Hide the 'category' field (parent part) - - Display parameter templates which are not yet related - """ - form = super().get_form() - - form.fields['category'].widget = HiddenInput() - form.fields['add_to_all_categories'].widget = HiddenInput() - form.fields['add_to_same_level_categories'].widget = HiddenInput() - - if form.is_valid(): - form.cleaned_data['category'] = self.kwargs.get('pk', None) - - try: - # Get selected category - category = PartCategory.objects.get(pk=self.kwargs.get('pk', None)) - # Get selected template - selected_template = self.get_object().parameter_template - - # Get existing parameter templates - parameters = [template.parameter_template.pk - for template in category.get_parameter_templates() - if template.parameter_template.pk != selected_template.pk] - - # Exclude templates already linked to category - updated_choices = [] - for choice in form.fields["parameter_template"].choices: - if (choice[0] not in parameters): - updated_choices.append(choice) - - # Update choices for parameter templates - form.fields['parameter_template'].choices = updated_choices - # Set initial choice to current template - form.fields['parameter_template'].initial = selected_template - except KeyError: - pass - - return form - - -class CategoryParameterTemplateDelete(AjaxDeleteView): - """View for deleting an existing PartCategoryParameterTemplate.""" - - model = PartCategoryParameterTemplate - ajax_form_title = _("Delete Category Parameter Template") - - def get_object(self): - """Returns the PartCategoryParameterTemplate associated with this view - - - First, attempt lookup based on supplied 'pid' kwarg - - Else, attempt lookup based on supplied 'pk' kwarg - """ - try: - self.object = self.model.objects.get(pk=self.kwargs['pid']) - except: - return None - - return self.object diff --git a/InvenTree/templates/InvenTree/settings/category.html b/InvenTree/templates/InvenTree/settings/category.html index f90d1e8d11..d6c5c872b0 100644 --- a/InvenTree/templates/InvenTree/settings/category.html +++ b/InvenTree/templates/InvenTree/settings/category.html @@ -8,14 +8,14 @@ {% endblock %} {% block actions %} - {% endblock %} {% block content %} -
+
diff --git a/InvenTree/templates/InvenTree/settings/settings.html b/InvenTree/templates/InvenTree/settings/settings.html index dc342fab3e..96236ec54d 100644 --- a/InvenTree/templates/InvenTree/settings/settings.html +++ b/InvenTree/templates/InvenTree/settings/settings.html @@ -222,7 +222,7 @@ $('#cat-param-table').inventreeTable({ switchable: false, }, { - field: 'parameter_template.name', + field: 'parameter_template_detail.name', title: '{% trans "Parameter Template" %}', sortable: 'true', }, @@ -249,18 +249,23 @@ $('#cat-param-table').inventreeTable({ function loadTemplateTable(pk) { - // Enable the buttons - $('#new-cat-param').removeAttr('disabled'); + var query = {}; + + if (pk) { + query['category'] = pk; + } // Load the parameter table $("#cat-param-table").bootstrapTable('refresh', { - query: { - category: pk, - }, + query: query, url: '{% url "api-part-category-parameter-list" %}', }); } + +// Initially load table with *all* categories +loadTemplateTable(); + $('body').on('change', '#category-select', function() { var pk = $(this).val(); loadTemplateTable(pk); @@ -270,14 +275,20 @@ $("#new-cat-param").click(function() { var pk = $('#category-select').val(); - launchModalForm(`/part/category/${pk}/parameters/new/`, { - success: function() { - $("#cat-param-table").bootstrapTable('refresh', { - query: { - category: pk, - } - }); + constructForm('{% url "api-part-category-parameter-list" %}', { + title: '{% trans "Create Category Parameter Template" %}', + method: 'POST', + fields: { + parameter_template: {}, + category: { + icon: 'fa-sitemap', + value: pk, + }, + default_value: {}, }, + onSuccess: function() { + loadTemplateTable(pk); + } }); }); @@ -286,15 +297,21 @@ $("#cat-param-table").on('click', '.template-edit', function() { var category = $('#category-select').val(); var pk = $(this).attr('pk'); - var url = `/part/category/${category}/parameters/${pk}/edit/`; - - launchModalForm(url, { - success: function() { - $("#cat-param-table").bootstrapTable('refresh'); + constructForm(`/api/part/category/parameters/${pk}/`, { + fields: { + parameter_template: {}, + category: { + icon: 'fa-sitemap', + }, + default_value: {}, + }, + onSuccess: function() { + loadTemplateTable(pk); } }); }); + $("#cat-param-table").on('click', '.template-delete', function() { var category = $('#category-select').val(); @@ -302,9 +319,11 @@ $("#cat-param-table").on('click', '.template-delete', function() { var url = `/part/category/${category}/parameters/${pk}/delete/`; - launchModalForm(url, { - success: function() { - $("#cat-param-table").bootstrapTable('refresh'); + constructForm(`/api/part/category/parameters/${pk}/`, { + method: 'DELETE', + title: '{% trans "Delete Category Parameter Template" %}', + onSuccess: function() { + loadTemplateTable(pk); } }); });