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 = 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
|
||||
|
||||
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
|
||||
- Allow "Attachment" list endpoints to be searched by attachment, link and comment fields
|
||||
|
||||
|
@ -45,7 +45,7 @@ def is_email_configured():
|
||||
configured = False
|
||||
|
||||
if not testing: # pragma: no cover
|
||||
logger.warning("DEFAULT_FROM_EMAIL is not configured")
|
||||
logger.debug("DEFAULT_FROM_EMAIL is not configured")
|
||||
|
||||
return configured
|
||||
|
||||
|
@ -742,6 +742,24 @@ class InvenTreeTree(MPTTModel):
|
||||
"""
|
||||
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):
|
||||
"""String representation of a category is the full path to that category."""
|
||||
return "{path} - {desc}".format(path=self.pathstring, desc=self.description)
|
||||
|
@ -190,6 +190,18 @@ class CategoryList(CategoryMixin, APIDownloadMixin, ListCreateAPI):
|
||||
class CategoryDetail(CategoryMixin, CustomRetrieveUpdateDestroyAPI):
|
||||
"""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):
|
||||
"""Perform 'update' function and mark this part as 'starred' (or not)"""
|
||||
# Clean up input data
|
||||
@ -1028,6 +1040,7 @@ class PartMixin:
|
||||
|
||||
kwargs['parameters'] = str2bool(params.get('parameters', None))
|
||||
kwargs['category_detail'] = str2bool(params.get('category_detail', False))
|
||||
kwargs['path_detail'] = str2bool(params.get('path_detail', False))
|
||||
|
||||
except AttributeError:
|
||||
pass
|
||||
|
@ -55,12 +55,23 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
'parent',
|
||||
'part_count',
|
||||
'pathstring',
|
||||
'path',
|
||||
'starred',
|
||||
'url',
|
||||
'structural',
|
||||
'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):
|
||||
"""Return True if the category is directly "starred" by the current user."""
|
||||
return category in self.context.get('starred_categories', [])
|
||||
@ -84,6 +95,12 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
starred = serializers.SerializerMethodField()
|
||||
|
||||
path = serializers.ListField(
|
||||
child=serializers.DictField(),
|
||||
source='get_path',
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for PartCategory tree."""
|
||||
@ -481,6 +498,7 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
||||
'barcode_hash',
|
||||
'category',
|
||||
'category_detail',
|
||||
'category_path',
|
||||
'component',
|
||||
'default_expiry',
|
||||
'default_location',
|
||||
@ -550,6 +568,7 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
||||
parameters = kwargs.pop('parameters', False)
|
||||
create = kwargs.pop('create', False)
|
||||
pricing = kwargs.pop('pricing', True)
|
||||
path_detail = kwargs.pop('path_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@ -559,6 +578,9 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
||||
if not parameters:
|
||||
self.fields.pop('parameters')
|
||||
|
||||
if not path_detail:
|
||||
self.fields.pop('category_path')
|
||||
|
||||
if not create:
|
||||
# These fields are only used for the LIST API endpoint
|
||||
for f in self.skip_create_fields()[1:]:
|
||||
@ -670,6 +692,12 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
||||
# Extra detail for the category
|
||||
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
|
||||
allocated_to_build_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()
|
||||
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):
|
||||
"""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['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):
|
||||
"""Unit tests for the Part List API endpoint"""
|
||||
|
@ -76,11 +76,18 @@ class StockDetail(RetrieveUpdateDestroyAPI):
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Set context before returning serializer."""
|
||||
kwargs['part_detail'] = True
|
||||
kwargs['location_detail'] = True
|
||||
kwargs['supplier_part_detail'] = True
|
||||
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)
|
||||
|
||||
|
||||
@ -1343,6 +1350,20 @@ class LocationDetail(CustomRetrieveUpdateDestroyAPI):
|
||||
queryset = StockLocation.objects.all()
|
||||
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):
|
||||
"""Return annotated queryset for the StockLocationList endpoint"""
|
||||
|
||||
|
@ -145,6 +145,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
'link',
|
||||
'location',
|
||||
'location_detail',
|
||||
'location_path',
|
||||
'notes',
|
||||
'owner',
|
||||
'packaging',
|
||||
@ -205,6 +206,12 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
label=_("Part"),
|
||||
)
|
||||
|
||||
location_path = serializers.ListField(
|
||||
child=serializers.DictField(),
|
||||
source='location.get_path',
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
"""
|
||||
Field used when creating a stock item
|
||||
"""
|
||||
@ -329,6 +336,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
location_detail = kwargs.pop('location_detail', False)
|
||||
supplier_part_detail = kwargs.pop('supplier_part_detail', False)
|
||||
tests = kwargs.pop('tests', False)
|
||||
path_detail = kwargs.pop('path_detail', False)
|
||||
|
||||
super(StockItemSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
@ -344,6 +352,9 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
if not tests:
|
||||
self.fields.pop('tests')
|
||||
|
||||
if not path_detail:
|
||||
self.fields.pop('location_path')
|
||||
|
||||
|
||||
class SerializeStockItemSerializer(serializers.Serializer):
|
||||
"""A DRF serializer for "serializing" a StockItem.
|
||||
@ -768,6 +779,7 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
'description',
|
||||
'parent',
|
||||
'pathstring',
|
||||
'path',
|
||||
'items',
|
||||
'owner',
|
||||
'icon',
|
||||
@ -781,6 +793,16 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
'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
|
||||
def annotate_queryset(queryset):
|
||||
"""Annotate extra information to the queryset"""
|
||||
@ -800,6 +822,12 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
path = serializers.ListField(
|
||||
child=serializers.DictField(),
|
||||
source='get_path',
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
|
||||
"""Serializer for StockItemAttachment model."""
|
||||
|
Loading…
Reference in New Issue
Block a user