mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 13:05:42 +00:00
Convert category parameter forms to use the API (#3130)
* Moving PartCategoryParameterTemplate model to the API - Add detail API endpoint - Add 'create' action to LIST endpoint * Update settings page to use the new API forms * Remove old views / forms * Update API version * Fix table buttons * Add title to deletion form * Add unit tests for new API views
This commit is contained in:
@ -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<pk>\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<pk>\d+)/', include([
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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<pid>\d+)/edit/', views.CategoryParameterTemplateEdit.as_view(), name='category-param-template-edit'),
|
||||
re_path(r'^(?P<pid>\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<pk>\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'),
|
||||
]))
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user