mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Path detail API (#5569)
* Add "path_detail" to stock location serializer - Requires ?path_detail=1 in URL query parameters - Only avaialable on the StockLocation detail URL endpoint (not list, too expensive) * Implement path_detail option for PartCategory detail API endpoint * Add "path_detail" option to PartSerializer * Add optional path_detail to StockItem serializer * Cleanup * Increment API version * Add unit test for Part and PartCategory * Remove debug statement
This commit is contained in:
parent
314c93d55c
commit
c60efd9a1d
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 134
|
INVENTREE_API_VERSION = 135
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v135 -> 2023-09-19 : https://github.com/inventree/InvenTree/pull/5569
|
||||||
|
- Adds location path detail to StockLocation and StockItem API endpoints
|
||||||
|
- Adds category path detail to PartCategory and Part API endpoints
|
||||||
|
|
||||||
v134 -> 2023-09-11 : https://github.com/inventree/InvenTree/pull/5525
|
v134 -> 2023-09-11 : https://github.com/inventree/InvenTree/pull/5525
|
||||||
- Allow "Attachment" list endpoints to be searched by attachment, link and comment fields
|
- Allow "Attachment" list endpoints to be searched by attachment, link and comment fields
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ def is_email_configured():
|
|||||||
configured = False
|
configured = False
|
||||||
|
|
||||||
if not testing: # pragma: no cover
|
if not testing: # pragma: no cover
|
||||||
logger.warning("DEFAULT_FROM_EMAIL is not configured")
|
logger.debug("DEFAULT_FROM_EMAIL is not configured")
|
||||||
|
|
||||||
return configured
|
return configured
|
||||||
|
|
||||||
|
@ -742,6 +742,24 @@ class InvenTreeTree(MPTTModel):
|
|||||||
"""
|
"""
|
||||||
return self.parentpath + [self]
|
return self.parentpath + [self]
|
||||||
|
|
||||||
|
def get_path(self):
|
||||||
|
"""Return a list of element in the item tree.
|
||||||
|
|
||||||
|
Contains the full path to this item, with each entry containing the following data:
|
||||||
|
|
||||||
|
{
|
||||||
|
pk: <pk>,
|
||||||
|
name: <name>,
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'pk': item.pk,
|
||||||
|
'name': item.name
|
||||||
|
} for item in self.path
|
||||||
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""String representation of a category is the full path to that category."""
|
"""String representation of a category is the full path to that category."""
|
||||||
return "{path} - {desc}".format(path=self.pathstring, desc=self.description)
|
return "{path} - {desc}".format(path=self.pathstring, desc=self.description)
|
||||||
|
@ -190,6 +190,18 @@ class CategoryList(CategoryMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
class CategoryDetail(CategoryMixin, CustomRetrieveUpdateDestroyAPI):
|
class CategoryDetail(CategoryMixin, CustomRetrieveUpdateDestroyAPI):
|
||||||
"""API endpoint for detail view of a single PartCategory object."""
|
"""API endpoint for detail view of a single PartCategory object."""
|
||||||
|
|
||||||
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
"""Add additional context based on query parameters"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = self.request.query_params
|
||||||
|
|
||||||
|
kwargs['path_detail'] = str2bool(params.get('path_detail', False))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
"""Perform 'update' function and mark this part as 'starred' (or not)"""
|
"""Perform 'update' function and mark this part as 'starred' (or not)"""
|
||||||
# Clean up input data
|
# Clean up input data
|
||||||
@ -1028,6 +1040,7 @@ class PartMixin:
|
|||||||
|
|
||||||
kwargs['parameters'] = str2bool(params.get('parameters', None))
|
kwargs['parameters'] = str2bool(params.get('parameters', None))
|
||||||
kwargs['category_detail'] = str2bool(params.get('category_detail', False))
|
kwargs['category_detail'] = str2bool(params.get('category_detail', False))
|
||||||
|
kwargs['path_detail'] = str2bool(params.get('path_detail', False))
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
@ -55,12 +55,23 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
'parent',
|
'parent',
|
||||||
'part_count',
|
'part_count',
|
||||||
'pathstring',
|
'pathstring',
|
||||||
|
'path',
|
||||||
'starred',
|
'starred',
|
||||||
'url',
|
'url',
|
||||||
'structural',
|
'structural',
|
||||||
'icon',
|
'icon',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Optionally add or remove extra fields"""
|
||||||
|
|
||||||
|
path_detail = kwargs.pop('path_detail', False)
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if not path_detail:
|
||||||
|
self.fields.pop('path')
|
||||||
|
|
||||||
def get_starred(self, category):
|
def get_starred(self, category):
|
||||||
"""Return True if the category is directly "starred" by the current user."""
|
"""Return True if the category is directly "starred" by the current user."""
|
||||||
return category in self.context.get('starred_categories', [])
|
return category in self.context.get('starred_categories', [])
|
||||||
@ -84,6 +95,12 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
|
|
||||||
starred = serializers.SerializerMethodField()
|
starred = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
path = serializers.ListField(
|
||||||
|
child=serializers.DictField(),
|
||||||
|
source='get_path',
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer):
|
class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer):
|
||||||
"""Serializer for PartCategory tree."""
|
"""Serializer for PartCategory tree."""
|
||||||
@ -481,6 +498,7 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
|||||||
'barcode_hash',
|
'barcode_hash',
|
||||||
'category',
|
'category',
|
||||||
'category_detail',
|
'category_detail',
|
||||||
|
'category_path',
|
||||||
'component',
|
'component',
|
||||||
'default_expiry',
|
'default_expiry',
|
||||||
'default_location',
|
'default_location',
|
||||||
@ -550,6 +568,7 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
|||||||
parameters = kwargs.pop('parameters', False)
|
parameters = kwargs.pop('parameters', False)
|
||||||
create = kwargs.pop('create', False)
|
create = kwargs.pop('create', False)
|
||||||
pricing = kwargs.pop('pricing', True)
|
pricing = kwargs.pop('pricing', True)
|
||||||
|
path_detail = kwargs.pop('path_detail', False)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@ -559,6 +578,9 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
|||||||
if not parameters:
|
if not parameters:
|
||||||
self.fields.pop('parameters')
|
self.fields.pop('parameters')
|
||||||
|
|
||||||
|
if not path_detail:
|
||||||
|
self.fields.pop('category_path')
|
||||||
|
|
||||||
if not create:
|
if not create:
|
||||||
# These fields are only used for the LIST API endpoint
|
# These fields are only used for the LIST API endpoint
|
||||||
for f in self.skip_create_fields()[1:]:
|
for f in self.skip_create_fields()[1:]:
|
||||||
@ -670,6 +692,12 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
|||||||
# Extra detail for the category
|
# Extra detail for the category
|
||||||
category_detail = CategorySerializer(source='category', many=False, read_only=True)
|
category_detail = CategorySerializer(source='category', many=False, read_only=True)
|
||||||
|
|
||||||
|
category_path = serializers.ListField(
|
||||||
|
child=serializers.DictField(),
|
||||||
|
source='category.get_path',
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
# Annotated fields
|
# Annotated fields
|
||||||
allocated_to_build_orders = serializers.FloatField(read_only=True)
|
allocated_to_build_orders = serializers.FloatField(read_only=True)
|
||||||
allocated_to_sales_orders = serializers.FloatField(read_only=True)
|
allocated_to_sales_orders = serializers.FloatField(read_only=True)
|
||||||
|
@ -441,6 +441,41 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
|||||||
part.refresh_from_db()
|
part.refresh_from_db()
|
||||||
self.assertEqual(part.category.pk, non_structural_category.pk)
|
self.assertEqual(part.category.pk, non_structural_category.pk)
|
||||||
|
|
||||||
|
def test_path_detail(self):
|
||||||
|
"""Test path_detail information"""
|
||||||
|
|
||||||
|
url = reverse('api-part-category-detail', kwargs={'pk': 5})
|
||||||
|
|
||||||
|
# First, request without path detail
|
||||||
|
response = self.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'path_detail': False,
|
||||||
|
},
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the path detail information is not included
|
||||||
|
self.assertFalse('path' in response.data.keys())
|
||||||
|
|
||||||
|
# Now, request *with* path detail
|
||||||
|
response = self.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'path_detail': True,
|
||||||
|
},
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue('path' in response.data.keys())
|
||||||
|
|
||||||
|
path = response.data['path']
|
||||||
|
|
||||||
|
self.assertEqual(len(path), 3)
|
||||||
|
self.assertEqual(path[0]['name'], 'Electronics')
|
||||||
|
self.assertEqual(path[1]['name'], 'IC')
|
||||||
|
self.assertEqual(path[2]['name'], 'MCU')
|
||||||
|
|
||||||
|
|
||||||
class PartOptionsAPITest(InvenTreeAPITestCase):
|
class PartOptionsAPITest(InvenTreeAPITestCase):
|
||||||
"""Tests for the various OPTIONS endpoints in the /part/ API.
|
"""Tests for the various OPTIONS endpoints in the /part/ API.
|
||||||
@ -1647,6 +1682,20 @@ class PartDetailTests(PartAPITestBase):
|
|||||||
self.assertEqual(data['in_stock'], 9000)
|
self.assertEqual(data['in_stock'], 9000)
|
||||||
self.assertEqual(data['unallocated_stock'], 9000)
|
self.assertEqual(data['unallocated_stock'], 9000)
|
||||||
|
|
||||||
|
def test_path_detail(self):
|
||||||
|
"""Check that path_detail can be requested against the serializer"""
|
||||||
|
|
||||||
|
response = self.get(
|
||||||
|
reverse('api-part-detail', kwargs={'pk': 1}),
|
||||||
|
{
|
||||||
|
'path_detail': True,
|
||||||
|
},
|
||||||
|
expected_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('category_path', response.data)
|
||||||
|
self.assertEqual(len(response.data['category_path']), 2)
|
||||||
|
|
||||||
|
|
||||||
class PartListTests(PartAPITestBase):
|
class PartListTests(PartAPITestBase):
|
||||||
"""Unit tests for the Part List API endpoint"""
|
"""Unit tests for the Part List API endpoint"""
|
||||||
|
@ -76,11 +76,18 @@ class StockDetail(RetrieveUpdateDestroyAPI):
|
|||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
"""Set context before returning serializer."""
|
"""Set context before returning serializer."""
|
||||||
kwargs['part_detail'] = True
|
|
||||||
kwargs['location_detail'] = True
|
|
||||||
kwargs['supplier_part_detail'] = True
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = self.request.query_params
|
||||||
|
|
||||||
|
kwargs['part_detail'] = str2bool(params.get('part_detail', True))
|
||||||
|
kwargs['location_detail'] = str2bool(params.get('location_detail', True))
|
||||||
|
kwargs['supplier_part_detail'] = str2bool(params.get('supplier_part_detail', True))
|
||||||
|
kwargs['path_detail'] = str2bool(params.get('path_detail', False))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@ -1343,6 +1350,20 @@ class LocationDetail(CustomRetrieveUpdateDestroyAPI):
|
|||||||
queryset = StockLocation.objects.all()
|
queryset = StockLocation.objects.all()
|
||||||
serializer_class = StockSerializers.LocationSerializer
|
serializer_class = StockSerializers.LocationSerializer
|
||||||
|
|
||||||
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
"""Add extra context to serializer based on provided query parameters"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = self.request.query_params
|
||||||
|
|
||||||
|
kwargs['path_detail'] = str2bool(params.get('path_detail', False))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
|
||||||
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self, *args, **kwargs):
|
def get_queryset(self, *args, **kwargs):
|
||||||
"""Return annotated queryset for the StockLocationList endpoint"""
|
"""Return annotated queryset for the StockLocationList endpoint"""
|
||||||
|
|
||||||
|
@ -145,6 +145,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
'link',
|
'link',
|
||||||
'location',
|
'location',
|
||||||
'location_detail',
|
'location_detail',
|
||||||
|
'location_path',
|
||||||
'notes',
|
'notes',
|
||||||
'owner',
|
'owner',
|
||||||
'packaging',
|
'packaging',
|
||||||
@ -205,6 +206,12 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
label=_("Part"),
|
label=_("Part"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
location_path = serializers.ListField(
|
||||||
|
child=serializers.DictField(),
|
||||||
|
source='location.get_path',
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Field used when creating a stock item
|
Field used when creating a stock item
|
||||||
"""
|
"""
|
||||||
@ -329,6 +336,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
location_detail = kwargs.pop('location_detail', False)
|
location_detail = kwargs.pop('location_detail', False)
|
||||||
supplier_part_detail = kwargs.pop('supplier_part_detail', False)
|
supplier_part_detail = kwargs.pop('supplier_part_detail', False)
|
||||||
tests = kwargs.pop('tests', False)
|
tests = kwargs.pop('tests', False)
|
||||||
|
path_detail = kwargs.pop('path_detail', False)
|
||||||
|
|
||||||
super(StockItemSerializer, self).__init__(*args, **kwargs)
|
super(StockItemSerializer, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@ -344,6 +352,9 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
if not tests:
|
if not tests:
|
||||||
self.fields.pop('tests')
|
self.fields.pop('tests')
|
||||||
|
|
||||||
|
if not path_detail:
|
||||||
|
self.fields.pop('location_path')
|
||||||
|
|
||||||
|
|
||||||
class SerializeStockItemSerializer(serializers.Serializer):
|
class SerializeStockItemSerializer(serializers.Serializer):
|
||||||
"""A DRF serializer for "serializing" a StockItem.
|
"""A DRF serializer for "serializing" a StockItem.
|
||||||
@ -768,6 +779,7 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
'description',
|
'description',
|
||||||
'parent',
|
'parent',
|
||||||
'pathstring',
|
'pathstring',
|
||||||
|
'path',
|
||||||
'items',
|
'items',
|
||||||
'owner',
|
'owner',
|
||||||
'icon',
|
'icon',
|
||||||
@ -781,6 +793,16 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
'barcode_hash',
|
'barcode_hash',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Optionally add or remove extra fields"""
|
||||||
|
|
||||||
|
path_detail = kwargs.pop('path_detail', False)
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if not path_detail:
|
||||||
|
self.fields.pop('path')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Annotate extra information to the queryset"""
|
"""Annotate extra information to the queryset"""
|
||||||
@ -800,6 +822,12 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
|
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
|
path = serializers.ListField(
|
||||||
|
child=serializers.DictField(),
|
||||||
|
source='get_path',
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
|
class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
|
||||||
"""Serializer for StockItemAttachment model."""
|
"""Serializer for StockItemAttachment model."""
|
||||||
|
Loading…
Reference in New Issue
Block a user