2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-25 10:27:39 +00:00

make can_filter suppport more complex scenarios:

- different filtername from fieldname
- multiple fields with one filtername
This commit is contained in:
Matthias Mair
2025-10-13 01:28:23 +02:00
parent 00abac5b78
commit c5b8344e1b
7 changed files with 233 additions and 286 deletions

View File

@@ -4,6 +4,7 @@ import os
from collections import OrderedDict from collections import OrderedDict
from copy import deepcopy from copy import deepcopy
from decimal import Decimal from decimal import Decimal
from typing import Optional
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
@@ -29,18 +30,18 @@ from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
# region path filtering # region path filtering
class OptionalFilterabelSerializer: class OptFilter:
"""Mixin to add context to serializer.""" """Filter for serializer or field."""
is_filterable = None is_filterable = None
is_filterable_default = None is_filterable_vals = {}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Initialize the serializer.""" """Initialize the serializer."""
# Set filterable options for future ref # Set filterable options for future ref
if self.is_filterable is None: if self.is_filterable is None:
self.is_filterable = kwargs.pop('is_filterable', None) self.is_filterable = kwargs.pop('is_filterable', None)
self.is_filterable_default = kwargs.pop('is_filterable_default', True) self.is_filterable_vals = kwargs.pop('is_filterable_vals', {})
# remove filter args from kwargs # remove filter args from kwargs
kwargs = PathScopedMixin.gather_filters(self, kwargs) kwargs = PathScopedMixin.gather_filters(self, kwargs)
@@ -69,7 +70,7 @@ class PathScopedMixin(serializers.Serializer):
# Actually gather the filterable fields # Actually gather the filterable fields
fields = self.fields.items() fields = self.fields.items()
self.filter_targets = { self.filter_targets = {
k: {'serializer': a, 'default': a.is_filterable_default} k: {'serializer': a, **getattr(a, 'is_filterable_vals', {})}
for k, a in fields for k, a in fields
if getattr(a, 'is_filterable', None) if getattr(a, 'is_filterable', None)
} }
@@ -93,18 +94,56 @@ class PathScopedMixin(serializers.Serializer):
# Decorator for marking serialzier fields that can be filtered out # Decorator for marking serialzier fields that can be filtered out
def can_filter(func, default=False): def can_filter(func, default=False, name: Optional[str] = None):
"""Decorator for marking serializer fields as filterable.""" """Decorator for marking serializer fields as filterable."""
is_field = False
# Check if function is holding OptionalFilterabelSerializer somehow # Check if function is holding OptionalFilterabelSerializer somehow
if not issubclass(func.__class__, OptionalFilterabelSerializer): if not issubclass(func.__class__, OptFilter):
raise TypeError( raise TypeError(
'can_filter can only be applied to OptionalFilterabelSerializer Serializers!' 'can_filter can only be applied to OptionalFilterabelSerializer Serializers!'
) )
# Mark the function as filterable
values = {'default': default, 'name': name if name else func.field_name}
if is_field:
pass
# print(func)
# func.is_filterable = True
# func.is_filterable_vals = values
else:
func._kwargs['is_filterable'] = True func._kwargs['is_filterable'] = True
func._kwargs['is_filterable_default'] = default func._kwargs['is_filterable_vals'] = values
# Add details
# TODO write names
# TODO aggregate pop fields
return func return func
class FilterableListSerializer(OptFilter, serializers.ListSerializer):
"""Custom ListSerializer which allows filtering of fields."""
class CfListField(OptFilter, serializers.ListField):
"""Custom ListField which allows filtering."""
class CfSerializerMethodField(OptFilter, serializers.SerializerMethodField):
"""Custom SerializerMethodField which allows filtering."""
class CfDateTimeField(OptFilter, serializers.DateTimeField):
"""Custom DateTimeField which allows filtering."""
class CfFloatField(OptFilter, serializers.FloatField):
"""Custom FloatField which allows filtering."""
class CfCharField(OptFilter, serializers.CharField):
"""Custom CharField which allows filtering."""
# endregion # endregion
@@ -112,7 +151,7 @@ class EmptySerializer(serializers.Serializer):
"""Empty serializer for use in testing.""" """Empty serializer for use in testing."""
class InvenTreeMoneySerializer(MoneyField): class InvenTreeMoneySerializer(OptFilter, MoneyField):
"""Custom serializer for 'MoneyField', which ensures that passed values are numerically valid. """Custom serializer for 'MoneyField', which ensures that passed values are numerically valid.
Ref: https://github.com/django-money/django-money/blob/master/djmoney/contrib/django_rest_framework/fields.py Ref: https://github.com/django-money/django-money/blob/master/djmoney/contrib/django_rest_framework/fields.py
@@ -302,9 +341,7 @@ class DependentField(serializers.Field):
return None return None
class InvenTreeModelSerializer( class InvenTreeModelSerializer(OptFilter, serializers.ModelSerializer):
OptionalFilterabelSerializer, serializers.ModelSerializer
):
"""Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation.""" """Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation."""
# Switch out URLField mapping # Switch out URLField mapping
@@ -610,9 +647,3 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
raise ValidationError(_('Failed to download image from remote URL')) raise ValidationError(_('Failed to download image from remote URL'))
return url return url
class FilterableListSerializer(
OptionalFilterabelSerializer, serializers.ListSerializer
):
"""Custom ListSerializer which allows filtering of fields."""

View File

@@ -31,8 +31,8 @@ from common.serializers import ProjectCodeSerializer
from common.settings import get_global_setting from common.settings import get_global_setting
from generic.states.fields import InvenTreeCustomStatusSerializerMixin from generic.states.fields import InvenTreeCustomStatusSerializerMixin
from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.ready import isGeneratingSchema
from InvenTree.serializers import ( from InvenTree.serializers import (
CfCharField,
FilterableListSerializer, FilterableListSerializer,
InvenTreeDecimalField, InvenTreeDecimalField,
InvenTreeModelSerializer, InvenTreeModelSerializer,
@@ -130,25 +130,39 @@ class BuildSerializer(
overdue = serializers.BooleanField(read_only=True, default=False) overdue = serializers.BooleanField(read_only=True, default=False)
issued_by_detail = UserSerializer(source='issued_by', read_only=True) issued_by_detail = can_filter(
UserSerializer(source='issued_by', read_only=True), True, name='user_detail'
)
responsible_detail = OwnerSerializer( responsible_detail = can_filter(
source='responsible', read_only=True, allow_null=True OwnerSerializer(source='responsible', read_only=True, allow_null=True),
True,
name='user_detail',
) )
barcode_hash = serializers.CharField(read_only=True) barcode_hash = serializers.CharField(read_only=True)
project_code_label = serializers.CharField( project_code_label = can_filter(
CfCharField(
source='project_code.code', source='project_code.code',
read_only=True, read_only=True,
label=_('Project Code Label'), label=_('Project Code Label'),
allow_null=True, allow_null=True,
),
True,
name='project_code_detail',
) )
project_code_detail = ProjectCodeSerializer( project_code_detail = can_filter(
ProjectCodeSerializer(
source='project_code', many=False, read_only=True, allow_null=True source='project_code', many=False, read_only=True, allow_null=True
),
True,
name='project_code_detail',
) )
project_code = can_filter(CfCharField(), True, name='project_code_detail')
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
"""Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible. """Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible.
@@ -172,27 +186,10 @@ class BuildSerializer(
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Determine if extra serializer fields are required.""" """Determine if extra serializer fields are required."""
user_detail = kwargs.pop('user_detail', True)
project_code_detail = kwargs.pop('project_code_detail', True)
kwargs.pop('create', False) kwargs.pop('create', False)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
# TODO INVE-T1 support complex filters
if not user_detail:
self.fields.pop('issued_by_detail', None)
self.fields.pop('responsible_detail', None)
# TODO INVE-T1 support complex filters
if not project_code_detail:
self.fields.pop('project_code', None)
self.fields.pop('project_code_label', None)
self.fields.pop('project_code_detail', None)
def validate_reference(self, reference): def validate_reference(self, reference):
"""Custom validation for the Build reference field.""" """Custom validation for the Build reference field."""
# Ensure the reference matches the required pattern # Ensure the reference matches the required pattern
@@ -1195,19 +1192,6 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
] ]
list_serializer_class = FilterableListSerializer list_serializer_class = FilterableListSerializer
def __init__(self, *args, **kwargs):
"""Determine which extra details fields should be included."""
stock_detail = kwargs.pop('stock_detail', True)
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
# TODO INVE-T1 support complex filters
if not stock_detail:
self.fields.pop('stock_item_detail', None)
# Export-only fields # Export-only fields
bom_reference = serializers.CharField( bom_reference = serializers.CharField(
source='build_line.bom_item.reference', label=_('BOM Reference'), read_only=True source='build_line.bom_item.reference', label=_('BOM Reference'), read_only=True
@@ -1245,7 +1229,8 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
True, True,
) )
stock_item_detail = StockItemSerializer( stock_item_detail = can_filter(
StockItemSerializer(
source='stock_item', source='stock_item',
read_only=True, read_only=True,
allow_null=True, allow_null=True,
@@ -1254,6 +1239,9 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
location_detail=False, location_detail=False,
supplier_part_detail=False, supplier_part_detail=False,
path_detail=False, path_detail=False,
),
True,
name='stock_detail',
) )
location = serializers.PrimaryKeyRelatedField( location = serializers.PrimaryKeyRelatedField(

View File

@@ -18,6 +18,7 @@ from importer.registry import register_importer
from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.ready import isGeneratingSchema from InvenTree.ready import isGeneratingSchema
from InvenTree.serializers import ( from InvenTree.serializers import (
CfCharField,
InvenTreeCurrencySerializer, InvenTreeCurrencySerializer,
InvenTreeDecimalField, InvenTreeDecimalField,
InvenTreeImageSerializerField, InvenTreeImageSerializerField,
@@ -274,19 +275,6 @@ class ManufacturerPartSerializer(
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
def __init__(self, *args, **kwargs):
"""Initialize this serializer with extra detail fields as required."""
prettify = kwargs.pop('pretty', False)
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
# TODO INVE-T1 support complex filters
if prettify is not True:
self.fields.pop('pretty_name', None)
part_detail = can_filter( part_detail = can_filter(
part_serializers.PartBriefSerializer( part_serializers.PartBriefSerializer(
source='part', many=False, read_only=True, allow_null=True source='part', many=False, read_only=True, allow_null=True
@@ -301,7 +289,9 @@ class ManufacturerPartSerializer(
True, True,
) )
pretty_name = serializers.CharField(read_only=True, allow_null=True) pretty_name = can_filter(
CfCharField(read_only=True, allow_null=True), name='prettify'
)
manufacturer = serializers.PrimaryKeyRelatedField( manufacturer = serializers.PrimaryKeyRelatedField(
queryset=Company.objects.filter(is_manufacturer=True) queryset=Company.objects.filter(is_manufacturer=True)

View File

@@ -27,7 +27,6 @@ import part.filters as part_filters
import part.models as part_models import part.models as part_models
import stock.models import stock.models
import stock.serializers import stock.serializers
import stock.status_codes
from common.serializers import ProjectCodeSerializer from common.serializers import ProjectCodeSerializer
from company.serializers import ( from company.serializers import (
AddressBriefSerializer, AddressBriefSerializer,
@@ -45,7 +44,6 @@ from InvenTree.helpers import (
str2bool, str2bool,
) )
from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.ready import isGeneratingSchema
from InvenTree.serializers import ( from InvenTree.serializers import (
InvenTreeCurrencySerializer, InvenTreeCurrencySerializer,
InvenTreeDecimalField, InvenTreeDecimalField,
@@ -505,20 +503,6 @@ class PurchaseOrderLineItemSerializer(
'internal_part_name', 'internal_part_name',
] ]
def __init__(self, *args, **kwargs):
"""Initialization routine for the serializer."""
part_detail = kwargs.pop('part_detail', False)
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
# TODO INVE-T1 support complex filters
if part_detail is not True:
self.fields.pop('part_detail', None)
self.fields.pop('supplier_part_detail', None)
def skip_create_fields(self): def skip_create_fields(self):
"""Return a list of fields to skip when creating a new object.""" """Return a list of fields to skip when creating a new object."""
return ['auto_pricing', 'merge_items', *super().skip_create_fields()] return ['auto_pricing', 'merge_items', *super().skip_create_fields()]
@@ -601,12 +585,18 @@ class PurchaseOrderLineItemSerializer(
total_price = serializers.FloatField(read_only=True) total_price = serializers.FloatField(read_only=True)
part_detail = PartBriefSerializer( part_detail = can_filter(
PartBriefSerializer(
source='get_base_part', many=False, read_only=True, allow_null=True source='get_base_part', many=False, read_only=True, allow_null=True
),
name='part_detail',
) )
supplier_part_detail = SupplierPartSerializer( supplier_part_detail = can_filter(
SupplierPartSerializer(
source='part', brief=True, many=False, read_only=True, allow_null=True source='part', brief=True, many=False, read_only=True, allow_null=True
),
name='part_detail',
) )
purchase_price = InvenTreeMoneySerializer(allow_null=True) purchase_price = InvenTreeMoneySerializer(allow_null=True)

View File

@@ -22,11 +22,9 @@ from sql_util.utils import SubqueryCount
from taggit.serializers import TagListSerializerField from taggit.serializers import TagListSerializerField
import common.currency import common.currency
import common.settings
import company.models import company.models
import InvenTree.helpers import InvenTree.helpers
import InvenTree.serializers import InvenTree.serializers
import InvenTree.status
import part.filters as part_filters import part.filters as part_filters
import part.helpers as part_helpers import part.helpers as part_helpers
import stock.models import stock.models
@@ -34,7 +32,13 @@ import users.models
from importer.registry import register_importer from importer.registry import register_importer
from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.ready import isGeneratingSchema from InvenTree.ready import isGeneratingSchema
from InvenTree.serializers import FilterableListSerializer, can_filter from InvenTree.serializers import (
CfDateTimeField,
CfFloatField,
CfListField,
FilterableListSerializer,
can_filter,
)
from users.serializers import UserSerializer from users.serializers import UserSerializer
from .models import ( from .models import (
@@ -86,16 +90,6 @@ class CategorySerializer(
] ]
read_only_fields = ['level', 'pathstring'] read_only_fields = ['level', 'pathstring']
def __init__(self, *args, **kwargs):
"""Optionally add or remove extra fields."""
path_detail = kwargs.pop('path_detail', False)
super().__init__(*args, **kwargs)
# TODO INVE-T1 support complex filters
if not path_detail and not isGeneratingSchema():
self.fields.pop('path', None)
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
"""Annotate extra information to the queryset.""" """Annotate extra information to the queryset."""
@@ -135,11 +129,14 @@ class CategorySerializer(
"""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', [])
path = serializers.ListField( path = can_filter(
CfListField(
child=serializers.DictField(), child=serializers.DictField(),
source='get_path', source='get_path',
read_only=True, read_only=True,
allow_null=True, allow_null=True,
),
name='path_detail',
) )
icon = serializers.CharField( icon = serializers.CharField(
@@ -352,17 +349,6 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
read_only_fields = ['barcode_hash'] read_only_fields = ['barcode_hash']
def __init__(self, *args, **kwargs):
"""Custom initialization routine for the PartBrief serializer."""
pricing = kwargs.pop('pricing', True)
super().__init__(*args, **kwargs)
# TODO INVE-T1 support complex filters
if not pricing and not isGeneratingSchema():
self.fields.pop('pricing_min', None)
self.fields.pop('pricing_max', None)
category_default_location = serializers.IntegerField( category_default_location = serializers.IntegerField(
read_only=True, allow_null=True read_only=True, allow_null=True
) )
@@ -384,11 +370,19 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
) )
# Pricing fields # Pricing fields
pricing_min = InvenTree.serializers.InvenTreeMoneySerializer( pricing_min = can_filter(
InvenTree.serializers.InvenTreeMoneySerializer(
source='pricing_data.overall_min', allow_null=True, read_only=True source='pricing_data.overall_min', allow_null=True, read_only=True
),
True,
name='pricing',
) )
pricing_max = InvenTree.serializers.InvenTreeMoneySerializer( pricing_max = can_filter(
InvenTree.serializers.InvenTreeMoneySerializer(
source='pricing_data.overall_max', allow_null=True, read_only=True source='pricing_data.overall_max', allow_null=True, read_only=True
),
True,
name='pricing',
) )
@@ -725,25 +719,11 @@ class PartSerializer(
- Allows us to optionally pass extra fields based on the query. - Allows us to optionally pass extra fields based on the query.
""" """
self.starred_parts = kwargs.pop('starred_parts', []) self.starred_parts = kwargs.pop('starred_parts', [])
location_detail = kwargs.pop('location_detail', False)
create = kwargs.pop('create', False) create = kwargs.pop('create', False)
pricing = kwargs.pop('pricing', True)
path_detail = kwargs.pop('path_detail', False)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if isGeneratingSchema(): if not create and not isGeneratingSchema():
return
# TODO INVE-T1 support complex filters
if not location_detail:
self.fields.pop('default_location_detail', None)
# TODO INVE-T1 support complex filters
if not path_detail:
self.fields.pop('category_path', None)
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(): for f in self.skip_create_fields():
# Fields required for certain operations, but are not part of the model # Fields required for certain operations, but are not part of the model
@@ -751,12 +731,6 @@ class PartSerializer(
continue continue
self.fields.pop(f, None) self.fields.pop(f, None)
# TODO INVE-T1 support complex filters
if not pricing:
self.fields.pop('pricing_min', None)
self.fields.pop('pricing_max', None)
self.fields.pop('pricing_updated', None)
def get_api_url(self): def get_api_url(self):
"""Return the API url associated with this serializer.""" """Return the API url associated with this serializer."""
return reverse_lazy('api-part-list') return reverse_lazy('api-part-list')
@@ -877,15 +851,21 @@ class PartSerializer(
) )
) )
category_path = serializers.ListField( category_path = can_filter(
CfListField(
child=serializers.DictField(), child=serializers.DictField(),
source='category.get_path', source='category.get_path',
read_only=True, read_only=True,
allow_null=True, allow_null=True,
),
name='path_detail',
) )
default_location_detail = DefaultLocationSerializer( default_location_detail = can_filter(
DefaultLocationSerializer(
source='default_location', many=False, read_only=True, allow_null=True source='default_location', many=False, read_only=True, allow_null=True
),
name='location_detail',
) )
category_name = serializers.CharField( category_name = serializers.CharField(
@@ -993,14 +973,24 @@ class PartSerializer(
) )
# Pricing fields # Pricing fields
pricing_min = InvenTree.serializers.InvenTreeMoneySerializer( pricing_min = can_filter(
InvenTree.serializers.InvenTreeMoneySerializer(
source='pricing_data.overall_min', allow_null=True, read_only=True source='pricing_data.overall_min', allow_null=True, read_only=True
),
True,
name='pricing',
) )
pricing_max = InvenTree.serializers.InvenTreeMoneySerializer( pricing_max = can_filter(
InvenTree.serializers.InvenTreeMoneySerializer(
source='pricing_data.overall_max', allow_null=True, read_only=True source='pricing_data.overall_max', allow_null=True, read_only=True
),
True,
name='pricing',
) )
pricing_updated = serializers.DateTimeField( pricing_updated = can_filter(
source='pricing_data.updated', allow_null=True, read_only=True CfDateTimeField(source='pricing_data.updated', allow_null=True, read_only=True),
True,
name='pricing',
) )
parameters = can_filter( parameters = can_filter(
@@ -1583,6 +1573,7 @@ class BomItemSubstituteSerializer(InvenTree.serializers.InvenTreeModelSerializer
model = BomItemSubstitute model = BomItemSubstitute
fields = ['pk', 'bom_item', 'part', 'part_detail'] fields = ['pk', 'bom_item', 'part', 'part_detail']
list_serializer_class = FilterableListSerializer
part_detail = PartBriefSerializer( part_detail = PartBriefSerializer(
source='part', read_only=True, many=False, pricing=False source='part', read_only=True, many=False, pricing=False
@@ -1646,33 +1637,6 @@ class BomItemSerializer(
] ]
list_serializer_class = FilterableListSerializer list_serializer_class = FilterableListSerializer
def __init__(self, *args, **kwargs):
"""Determine if extra detail fields are to be annotated on this serializer.
- part_detail and sub_part_detail serializers are only included if requested.
- This saves a bunch of database requests
"""
can_build = kwargs.pop('can_build', True)
pricing = kwargs.pop('pricing', True)
substitutes = kwargs.pop('substitutes', True)
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if not substitutes:
self.fields.pop('substitutes', None)
if not can_build:
self.fields.pop('can_build')
# TODO INVE-T1 support complex filters
if not pricing:
self.fields.pop('pricing_min', None)
self.fields.pop('pricing_max', None)
self.fields.pop('pricing_min_total', None)
self.fields.pop('pricing_max_total', None)
self.fields.pop('pricing_updated', None)
quantity = InvenTree.serializers.InvenTreeDecimalField(required=True) quantity = InvenTree.serializers.InvenTreeDecimalField(required=True)
setup_quantity = InvenTree.serializers.InvenTreeDecimalField(required=False) setup_quantity = InvenTree.serializers.InvenTreeDecimalField(required=False)
@@ -1696,8 +1660,8 @@ class BomItemSerializer(
help_text=_('Select the parent assembly'), help_text=_('Select the parent assembly'),
) )
substitutes = BomItemSubstituteSerializer( substitutes = can_filter(
many=True, read_only=True, allow_null=True BomItemSubstituteSerializer(many=True, read_only=True, allow_null=True), True
) )
part_detail = can_filter( part_detail = can_filter(
@@ -1735,28 +1699,41 @@ class BomItemSerializer(
label=_('In Production'), read_only=True, allow_null=True label=_('In Production'), read_only=True, allow_null=True
) )
can_build = serializers.FloatField( can_build = can_filter(
label=_('Can Build'), read_only=True, allow_null=True CfFloatField(label=_('Can Build'), read_only=True, allow_null=True), True
) )
# Cached pricing fields # Cached pricing fields
pricing_min = InvenTree.serializers.InvenTreeMoneySerializer( pricing_min = can_filter(
InvenTree.serializers.InvenTreeMoneySerializer(
source='sub_part.pricing_data.overall_min', allow_null=True, read_only=True source='sub_part.pricing_data.overall_min', allow_null=True, read_only=True
),
True,
name='pricing',
) )
pricing_max = can_filter(
pricing_max = InvenTree.serializers.InvenTreeMoneySerializer( InvenTree.serializers.InvenTreeMoneySerializer(
source='sub_part.pricing_data.overall_max', allow_null=True, read_only=True source='sub_part.pricing_data.overall_max', allow_null=True, read_only=True
),
True,
name='pricing',
) )
pricing_min_total = can_filter(
pricing_min_total = InvenTree.serializers.InvenTreeMoneySerializer( InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True),
allow_null=True, read_only=True True,
name='pricing',
) )
pricing_max_total = InvenTree.serializers.InvenTreeMoneySerializer( pricing_max_total = can_filter(
allow_null=True, read_only=True InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True),
True,
name='pricing',
) )
pricing_updated = can_filter(
pricing_updated = serializers.DateTimeField( CfDateTimeField(
source='sub_part.pricing_data.updated', allow_null=True, read_only=True source='sub_part.pricing_data.updated', allow_null=True, read_only=True
),
True,
name='pricing',
) )
# Annotated fields for available stock # Annotated fields for available stock

View File

@@ -32,8 +32,9 @@ from common.settings import get_global_setting
from generic.states.fields import InvenTreeCustomStatusSerializerMixin from generic.states.fields import InvenTreeCustomStatusSerializerMixin
from importer.registry import register_importer from importer.registry import register_importer
from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.ready import isGeneratingSchema
from InvenTree.serializers import ( from InvenTree.serializers import (
CfListField,
FilterableListSerializer,
InvenTreeCurrencySerializer, InvenTreeCurrencySerializer,
InvenTreeDecimalField, InvenTreeDecimalField,
InvenTreeModelSerializer, InvenTreeModelSerializer,
@@ -204,7 +205,6 @@ class StockItemTestResultSerializer(
"""Metaclass options.""" """Metaclass options."""
model = StockItemTestResult model = StockItemTestResult
fields = [ fields = [
'pk', 'pk',
'stock_item', 'stock_item',
@@ -221,8 +221,8 @@ class StockItemTestResultSerializer(
'template', 'template',
'template_detail', 'template_detail',
] ]
read_only_fields = ['pk', 'user', 'date'] read_only_fields = ['pk', 'user', 'date']
list_serializer_class = FilterableListSerializer
user_detail = can_filter( user_detail = can_filter(
UserSerializer(source='user', read_only=True, allow_null=True) UserSerializer(source='user', read_only=True, allow_null=True)
@@ -403,23 +403,6 @@ class StockItemSerializer(
'serial_numbers': {'write_only': True}, 'serial_numbers': {'write_only': True},
} }
def __init__(self, *args, **kwargs):
"""Add detail fields."""
path_detail = kwargs.pop('path_detail', False)
tests = kwargs.pop('tests', False)
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if not tests:
self.fields.pop('tests', None)
# TODO INVE-T1 support complex filters
if not path_detail:
self.fields.pop('location_path', None)
part = serializers.PrimaryKeyRelatedField( part = serializers.PrimaryKeyRelatedField(
queryset=part_models.Part.objects.all(), queryset=part_models.Part.objects.all(),
many=False, many=False,
@@ -435,11 +418,14 @@ class StockItemSerializer(
help_text=_('Parent stock item'), help_text=_('Parent stock item'),
) )
location_path = serializers.ListField( location_path = can_filter(
CfListField(
child=serializers.DictField(), child=serializers.DictField(),
source='location.get_path', source='location.get_path',
read_only=True, read_only=True,
allow_null=True, allow_null=True,
),
name='path_detail',
) )
in_stock = serializers.BooleanField(read_only=True, label=_('In Stock')) in_stock = serializers.BooleanField(read_only=True, label=_('In Stock'))
@@ -624,9 +610,11 @@ class StockItemSerializer(
True, True,
) )
tests = StockItemTestResultSerializer( tests = can_filter(
StockItemTestResultSerializer(
source='test_results', many=True, read_only=True, allow_null=True source='test_results', many=True, read_only=True, allow_null=True
) )
)
quantity = InvenTreeDecimalField() quantity = InvenTreeDecimalField()
@@ -1170,16 +1158,6 @@ class LocationSerializer(
read_only_fields = ['barcode_hash', 'icon', 'level', 'pathstring'] read_only_fields = ['barcode_hash', 'icon', 'level', 'pathstring']
def __init__(self, *args, **kwargs):
"""Optionally add or remove extra fields."""
path_detail = kwargs.pop('path_detail', False)
super().__init__(*args, **kwargs)
# TODO INVE-T1 support complex filters
if not path_detail and not isGeneratingSchema():
self.fields.pop('path', None)
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
"""Annotate extra information to the queryset.""" """Annotate extra information to the queryset."""
@@ -1211,11 +1189,14 @@ class LocationSerializer(
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
path = serializers.ListField( path = can_filter(
CfListField(
child=serializers.DictField(), child=serializers.DictField(),
source='get_path', source='get_path',
read_only=True, read_only=True,
allow_null=True, allow_null=True,
),
name='path_detail',
) )
# explicitly set this field, so it gets included for AutoSchema # explicitly set this field, so it gets included for AutoSchema

View File

@@ -1,15 +1,18 @@
"""DRF API serializers for the 'users' app.""" """DRF API serializers for the 'users' app."""
from django.contrib.auth.models import Group, Permission, User from django.contrib.auth.models import Group, Permission, User
from django.core.exceptions import AppRegistryNotReady
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from InvenTree.ready import isGeneratingSchema from InvenTree.serializers import (
from InvenTree.serializers import InvenTreeModelSerializer CfSerializerMethodField,
FilterableListSerializer,
InvenTreeModelSerializer,
can_filter,
)
from .models import ApiToken, Owner, RuleSet, UserProfile from .models import ApiToken, Owner, RuleSet, UserProfile
from .permissions import check_user_role from .permissions import check_user_role
@@ -49,6 +52,7 @@ class RuleSetSerializer(InvenTreeModelSerializer):
'can_delete', 'can_delete',
] ]
read_only_fields = ['pk', 'name', 'label', 'group'] read_only_fields = ['pk', 'name', 'label', 'group']
list_serializer_class = FilterableListSerializer
class RoleSerializer(InvenTreeModelSerializer): class RoleSerializer(InvenTreeModelSerializer):
@@ -173,8 +177,8 @@ class UserSerializer(InvenTreeModelSerializer):
model = User model = User
fields = ['pk', 'username', 'first_name', 'last_name', 'email'] fields = ['pk', 'username', 'first_name', 'last_name', 'email']
read_only_fields = ['username', 'email'] read_only_fields = ['username', 'email']
list_serializer_class = FilterableListSerializer
username = serializers.CharField(label=_('Username'), help_text=_('Username')) username = serializers.CharField(label=_('Username'), help_text=_('Username'))
@@ -243,39 +247,25 @@ class GroupSerializer(InvenTreeModelSerializer):
model = Group model = Group
fields = ['pk', 'name', 'permissions', 'roles', 'users'] fields = ['pk', 'name', 'permissions', 'roles', 'users']
def __init__(self, *args, **kwargs): permissions = can_filter(
"""Initialize this serializer with extra fields as required.""" CfSerializerMethodField(allow_null=True, read_only=True),
# TODO INVE-T1 support complex filters name='permission_detail',
role_detail = kwargs.pop('role_detail', False) )
user_detail = kwargs.pop('user_detail', False)
permission_detail = kwargs.pop('permission_detail', False)
super().__init__(*args, **kwargs)
try:
if not isGeneratingSchema():
if not permission_detail:
self.fields.pop('permissions', None)
if not role_detail:
self.fields.pop('roles', None)
if not user_detail:
self.fields.pop('users', None)
except AppRegistryNotReady: # pragma: no cover
pass
permissions = serializers.SerializerMethodField(allow_null=True, read_only=True)
def get_permissions(self, group: Group) -> dict: def get_permissions(self, group: Group) -> dict:
"""Return a list of permissions associated with the group.""" """Return a list of permissions associated with the group."""
return generate_permission_dict(group.permissions.all()) return generate_permission_dict(group.permissions.all())
roles = RuleSetSerializer( roles = can_filter(
RuleSetSerializer(
source='rule_sets', many=True, read_only=True, allow_null=True source='rule_sets', many=True, read_only=True, allow_null=True
),
name='role_detail',
) )
users = UserSerializer( users = can_filter(
source='user_set', many=True, read_only=True, allow_null=True UserSerializer(source='user_set', many=True, read_only=True, allow_null=True),
name='user_detail',
) )