diff --git a/src/backend/InvenTree/InvenTree/serializers.py b/src/backend/InvenTree/InvenTree/serializers.py index f31504a508..beb11dddc0 100644 --- a/src/backend/InvenTree/InvenTree/serializers.py +++ b/src/backend/InvenTree/InvenTree/serializers.py @@ -4,6 +4,7 @@ import os from collections import OrderedDict from copy import deepcopy from decimal import Decimal +from typing import Optional from django.conf import settings from django.core.exceptions import ValidationError as DjangoValidationError @@ -29,18 +30,18 @@ from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField # region path filtering -class OptionalFilterabelSerializer: - """Mixin to add context to serializer.""" +class OptFilter: + """Filter for serializer or field.""" is_filterable = None - is_filterable_default = None + is_filterable_vals = {} def __init__(self, *args, **kwargs): """Initialize the serializer.""" # Set filterable options for future ref if self.is_filterable is 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 kwargs = PathScopedMixin.gather_filters(self, kwargs) @@ -69,7 +70,7 @@ class PathScopedMixin(serializers.Serializer): # Actually gather the filterable fields fields = self.fields.items() self.filter_targets = { - k: {'serializer': a, 'default': a.is_filterable_default} + k: {'serializer': a, **getattr(a, 'is_filterable_vals', {})} for k, a in fields if getattr(a, 'is_filterable', None) } @@ -93,18 +94,56 @@ class PathScopedMixin(serializers.Serializer): # 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.""" + is_field = False # Check if function is holding OptionalFilterabelSerializer somehow - if not issubclass(func.__class__, OptionalFilterabelSerializer): + if not issubclass(func.__class__, OptFilter): raise TypeError( 'can_filter can only be applied to OptionalFilterabelSerializer Serializers!' ) - func._kwargs['is_filterable'] = True - func._kwargs['is_filterable_default'] = default + + # 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_vals'] = values + # Add details + # TODO write names + # TODO aggregate pop fields 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 @@ -112,7 +151,7 @@ class EmptySerializer(serializers.Serializer): """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. 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 -class InvenTreeModelSerializer( - OptionalFilterabelSerializer, serializers.ModelSerializer -): +class InvenTreeModelSerializer(OptFilter, serializers.ModelSerializer): """Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation.""" # Switch out URLField mapping @@ -610,9 +647,3 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass): raise ValidationError(_('Failed to download image from remote URL')) return url - - -class FilterableListSerializer( - OptionalFilterabelSerializer, serializers.ListSerializer -): - """Custom ListSerializer which allows filtering of fields.""" diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index 19b7efa61f..374b81570a 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -31,8 +31,8 @@ from common.serializers import ProjectCodeSerializer from common.settings import get_global_setting from generic.states.fields import InvenTreeCustomStatusSerializerMixin from InvenTree.mixins import DataImportExportSerializerMixin -from InvenTree.ready import isGeneratingSchema from InvenTree.serializers import ( + CfCharField, FilterableListSerializer, InvenTreeDecimalField, InvenTreeModelSerializer, @@ -130,25 +130,39 @@ class BuildSerializer( 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( - source='responsible', read_only=True, allow_null=True + responsible_detail = can_filter( + OwnerSerializer(source='responsible', read_only=True, allow_null=True), + True, + name='user_detail', ) barcode_hash = serializers.CharField(read_only=True) - project_code_label = serializers.CharField( - source='project_code.code', - read_only=True, - label=_('Project Code Label'), - allow_null=True, + project_code_label = can_filter( + CfCharField( + source='project_code.code', + read_only=True, + label=_('Project Code Label'), + allow_null=True, + ), + True, + name='project_code_detail', ) - project_code_detail = ProjectCodeSerializer( - source='project_code', many=False, read_only=True, allow_null=True + project_code_detail = can_filter( + ProjectCodeSerializer( + 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 def annotate_queryset(queryset): """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): """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) 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): """Custom validation for the Build reference field.""" # Ensure the reference matches the required pattern @@ -1195,19 +1192,6 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali ] 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 bom_reference = serializers.CharField( source='build_line.bom_item.reference', label=_('BOM Reference'), read_only=True @@ -1245,15 +1229,19 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali True, ) - stock_item_detail = StockItemSerializer( - source='stock_item', - read_only=True, - allow_null=True, - label=_('Stock Item'), - part_detail=False, - location_detail=False, - supplier_part_detail=False, - path_detail=False, + stock_item_detail = can_filter( + StockItemSerializer( + source='stock_item', + read_only=True, + allow_null=True, + label=_('Stock Item'), + part_detail=False, + location_detail=False, + supplier_part_detail=False, + path_detail=False, + ), + True, + name='stock_detail', ) location = serializers.PrimaryKeyRelatedField( diff --git a/src/backend/InvenTree/company/serializers.py b/src/backend/InvenTree/company/serializers.py index d51dc1603d..6b44de7b7b 100644 --- a/src/backend/InvenTree/company/serializers.py +++ b/src/backend/InvenTree/company/serializers.py @@ -18,6 +18,7 @@ from importer.registry import register_importer from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.ready import isGeneratingSchema from InvenTree.serializers import ( + CfCharField, InvenTreeCurrencySerializer, InvenTreeDecimalField, InvenTreeImageSerializerField, @@ -274,19 +275,6 @@ class ManufacturerPartSerializer( 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_serializers.PartBriefSerializer( source='part', many=False, read_only=True, allow_null=True @@ -301,7 +289,9 @@ class ManufacturerPartSerializer( 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( queryset=Company.objects.filter(is_manufacturer=True) diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 3154ded6f7..d698806507 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -27,7 +27,6 @@ import part.filters as part_filters import part.models as part_models import stock.models import stock.serializers -import stock.status_codes from common.serializers import ProjectCodeSerializer from company.serializers import ( AddressBriefSerializer, @@ -45,7 +44,6 @@ from InvenTree.helpers import ( str2bool, ) from InvenTree.mixins import DataImportExportSerializerMixin -from InvenTree.ready import isGeneratingSchema from InvenTree.serializers import ( InvenTreeCurrencySerializer, InvenTreeDecimalField, @@ -505,20 +503,6 @@ class PurchaseOrderLineItemSerializer( '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): """Return a list of fields to skip when creating a new object.""" return ['auto_pricing', 'merge_items', *super().skip_create_fields()] @@ -601,12 +585,18 @@ class PurchaseOrderLineItemSerializer( total_price = serializers.FloatField(read_only=True) - part_detail = PartBriefSerializer( - source='get_base_part', many=False, read_only=True, allow_null=True + part_detail = can_filter( + PartBriefSerializer( + source='get_base_part', many=False, read_only=True, allow_null=True + ), + name='part_detail', ) - supplier_part_detail = SupplierPartSerializer( - source='part', brief=True, many=False, read_only=True, allow_null=True + supplier_part_detail = can_filter( + SupplierPartSerializer( + source='part', brief=True, many=False, read_only=True, allow_null=True + ), + name='part_detail', ) purchase_price = InvenTreeMoneySerializer(allow_null=True) diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index 350d849203..104690f259 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -22,11 +22,9 @@ from sql_util.utils import SubqueryCount from taggit.serializers import TagListSerializerField import common.currency -import common.settings import company.models import InvenTree.helpers import InvenTree.serializers -import InvenTree.status import part.filters as part_filters import part.helpers as part_helpers import stock.models @@ -34,7 +32,13 @@ import users.models from importer.registry import register_importer from InvenTree.mixins import DataImportExportSerializerMixin 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 .models import ( @@ -86,16 +90,6 @@ class CategorySerializer( ] 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 def annotate_queryset(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 category in self.context.get('starred_categories', []) - path = serializers.ListField( - child=serializers.DictField(), - source='get_path', - read_only=True, - allow_null=True, + path = can_filter( + CfListField( + child=serializers.DictField(), + source='get_path', + read_only=True, + allow_null=True, + ), + name='path_detail', ) icon = serializers.CharField( @@ -352,17 +349,6 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer): 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( read_only=True, allow_null=True ) @@ -384,11 +370,19 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer): ) # Pricing fields - pricing_min = InvenTree.serializers.InvenTreeMoneySerializer( - source='pricing_data.overall_min', allow_null=True, read_only=True + pricing_min = can_filter( + InvenTree.serializers.InvenTreeMoneySerializer( + source='pricing_data.overall_min', allow_null=True, read_only=True + ), + True, + name='pricing', ) - pricing_max = InvenTree.serializers.InvenTreeMoneySerializer( - source='pricing_data.overall_max', allow_null=True, read_only=True + pricing_max = can_filter( + InvenTree.serializers.InvenTreeMoneySerializer( + 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. """ self.starred_parts = kwargs.pop('starred_parts', []) - location_detail = kwargs.pop('location_detail', False) create = kwargs.pop('create', False) - pricing = kwargs.pop('pricing', True) - path_detail = kwargs.pop('path_detail', False) super().__init__(*args, **kwargs) - if 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: + if not create and not isGeneratingSchema(): # These fields are only used for the LIST API endpoint for f in self.skip_create_fields(): # Fields required for certain operations, but are not part of the model @@ -751,12 +731,6 @@ class PartSerializer( continue 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): """Return the API url associated with this serializer.""" return reverse_lazy('api-part-list') @@ -877,15 +851,21 @@ class PartSerializer( ) ) - category_path = serializers.ListField( - child=serializers.DictField(), - source='category.get_path', - read_only=True, - allow_null=True, + category_path = can_filter( + CfListField( + child=serializers.DictField(), + source='category.get_path', + read_only=True, + allow_null=True, + ), + name='path_detail', ) - default_location_detail = DefaultLocationSerializer( - source='default_location', many=False, read_only=True, allow_null=True + default_location_detail = can_filter( + DefaultLocationSerializer( + source='default_location', many=False, read_only=True, allow_null=True + ), + name='location_detail', ) category_name = serializers.CharField( @@ -993,14 +973,24 @@ class PartSerializer( ) # Pricing fields - pricing_min = InvenTree.serializers.InvenTreeMoneySerializer( - source='pricing_data.overall_min', allow_null=True, read_only=True + pricing_min = can_filter( + InvenTree.serializers.InvenTreeMoneySerializer( + source='pricing_data.overall_min', allow_null=True, read_only=True + ), + True, + name='pricing', ) - pricing_max = InvenTree.serializers.InvenTreeMoneySerializer( - source='pricing_data.overall_max', allow_null=True, read_only=True + pricing_max = can_filter( + InvenTree.serializers.InvenTreeMoneySerializer( + source='pricing_data.overall_max', allow_null=True, read_only=True + ), + True, + name='pricing', ) - pricing_updated = serializers.DateTimeField( - source='pricing_data.updated', allow_null=True, read_only=True + pricing_updated = can_filter( + CfDateTimeField(source='pricing_data.updated', allow_null=True, read_only=True), + True, + name='pricing', ) parameters = can_filter( @@ -1583,6 +1573,7 @@ class BomItemSubstituteSerializer(InvenTree.serializers.InvenTreeModelSerializer model = BomItemSubstitute fields = ['pk', 'bom_item', 'part', 'part_detail'] + list_serializer_class = FilterableListSerializer part_detail = PartBriefSerializer( source='part', read_only=True, many=False, pricing=False @@ -1646,33 +1637,6 @@ class BomItemSerializer( ] 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) setup_quantity = InvenTree.serializers.InvenTreeDecimalField(required=False) @@ -1696,8 +1660,8 @@ class BomItemSerializer( help_text=_('Select the parent assembly'), ) - substitutes = BomItemSubstituteSerializer( - many=True, read_only=True, allow_null=True + substitutes = can_filter( + BomItemSubstituteSerializer(many=True, read_only=True, allow_null=True), True ) part_detail = can_filter( @@ -1735,28 +1699,41 @@ class BomItemSerializer( label=_('In Production'), read_only=True, allow_null=True ) - can_build = serializers.FloatField( - label=_('Can Build'), read_only=True, allow_null=True + can_build = can_filter( + CfFloatField(label=_('Can Build'), read_only=True, allow_null=True), True ) # Cached pricing fields - pricing_min = InvenTree.serializers.InvenTreeMoneySerializer( - source='sub_part.pricing_data.overall_min', allow_null=True, read_only=True + pricing_min = can_filter( + InvenTree.serializers.InvenTreeMoneySerializer( + source='sub_part.pricing_data.overall_min', allow_null=True, read_only=True + ), + True, + name='pricing', ) - - pricing_max = InvenTree.serializers.InvenTreeMoneySerializer( - source='sub_part.pricing_data.overall_max', allow_null=True, read_only=True + pricing_max = can_filter( + InvenTree.serializers.InvenTreeMoneySerializer( + source='sub_part.pricing_data.overall_max', allow_null=True, read_only=True + ), + True, + name='pricing', ) - - pricing_min_total = InvenTree.serializers.InvenTreeMoneySerializer( - allow_null=True, read_only=True + pricing_min_total = can_filter( + InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True), + True, + name='pricing', ) - pricing_max_total = InvenTree.serializers.InvenTreeMoneySerializer( - allow_null=True, read_only=True + pricing_max_total = can_filter( + InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True), + True, + name='pricing', ) - - pricing_updated = serializers.DateTimeField( - source='sub_part.pricing_data.updated', allow_null=True, read_only=True + pricing_updated = can_filter( + CfDateTimeField( + source='sub_part.pricing_data.updated', allow_null=True, read_only=True + ), + True, + name='pricing', ) # Annotated fields for available stock diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index eaa5be1972..b0e28199f7 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -32,8 +32,9 @@ from common.settings import get_global_setting from generic.states.fields import InvenTreeCustomStatusSerializerMixin from importer.registry import register_importer from InvenTree.mixins import DataImportExportSerializerMixin -from InvenTree.ready import isGeneratingSchema from InvenTree.serializers import ( + CfListField, + FilterableListSerializer, InvenTreeCurrencySerializer, InvenTreeDecimalField, InvenTreeModelSerializer, @@ -204,7 +205,6 @@ class StockItemTestResultSerializer( """Metaclass options.""" model = StockItemTestResult - fields = [ 'pk', 'stock_item', @@ -221,8 +221,8 @@ class StockItemTestResultSerializer( 'template', 'template_detail', ] - read_only_fields = ['pk', 'user', 'date'] + list_serializer_class = FilterableListSerializer user_detail = can_filter( UserSerializer(source='user', read_only=True, allow_null=True) @@ -403,23 +403,6 @@ class StockItemSerializer( '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( queryset=part_models.Part.objects.all(), many=False, @@ -435,11 +418,14 @@ class StockItemSerializer( help_text=_('Parent stock item'), ) - location_path = serializers.ListField( - child=serializers.DictField(), - source='location.get_path', - read_only=True, - allow_null=True, + location_path = can_filter( + CfListField( + child=serializers.DictField(), + source='location.get_path', + read_only=True, + allow_null=True, + ), + name='path_detail', ) in_stock = serializers.BooleanField(read_only=True, label=_('In Stock')) @@ -624,8 +610,10 @@ class StockItemSerializer( True, ) - tests = StockItemTestResultSerializer( - source='test_results', many=True, read_only=True, allow_null=True + tests = can_filter( + StockItemTestResultSerializer( + source='test_results', many=True, read_only=True, allow_null=True + ) ) quantity = InvenTreeDecimalField() @@ -1170,16 +1158,6 @@ class LocationSerializer( 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 def annotate_queryset(queryset): """Annotate extra information to the queryset.""" @@ -1211,11 +1189,14 @@ class LocationSerializer( tags = TagListSerializerField(required=False) - path = serializers.ListField( - child=serializers.DictField(), - source='get_path', - read_only=True, - allow_null=True, + path = can_filter( + CfListField( + child=serializers.DictField(), + source='get_path', + read_only=True, + allow_null=True, + ), + name='path_detail', ) # explicitly set this field, so it gets included for AutoSchema diff --git a/src/backend/InvenTree/users/serializers.py b/src/backend/InvenTree/users/serializers.py index 8d09eb9ad1..00d5db2686 100644 --- a/src/backend/InvenTree/users/serializers.py +++ b/src/backend/InvenTree/users/serializers.py @@ -1,15 +1,18 @@ """DRF API serializers for the 'users' app.""" from django.contrib.auth.models import Group, Permission, User -from django.core.exceptions import AppRegistryNotReady from django.db.models import Q from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from rest_framework.exceptions import PermissionDenied -from InvenTree.ready import isGeneratingSchema -from InvenTree.serializers import InvenTreeModelSerializer +from InvenTree.serializers import ( + CfSerializerMethodField, + FilterableListSerializer, + InvenTreeModelSerializer, + can_filter, +) from .models import ApiToken, Owner, RuleSet, UserProfile from .permissions import check_user_role @@ -49,6 +52,7 @@ class RuleSetSerializer(InvenTreeModelSerializer): 'can_delete', ] read_only_fields = ['pk', 'name', 'label', 'group'] + list_serializer_class = FilterableListSerializer class RoleSerializer(InvenTreeModelSerializer): @@ -173,8 +177,8 @@ class UserSerializer(InvenTreeModelSerializer): model = User fields = ['pk', 'username', 'first_name', 'last_name', 'email'] - read_only_fields = ['username', 'email'] + list_serializer_class = FilterableListSerializer username = serializers.CharField(label=_('Username'), help_text=_('Username')) @@ -243,39 +247,25 @@ class GroupSerializer(InvenTreeModelSerializer): model = Group fields = ['pk', 'name', 'permissions', 'roles', 'users'] - def __init__(self, *args, **kwargs): - """Initialize this serializer with extra fields as required.""" - # TODO INVE-T1 support complex filters - 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) + permissions = can_filter( + CfSerializerMethodField(allow_null=True, read_only=True), + name='permission_detail', + ) def get_permissions(self, group: Group) -> dict: """Return a list of permissions associated with the group.""" return generate_permission_dict(group.permissions.all()) - roles = RuleSetSerializer( - source='rule_sets', many=True, read_only=True, allow_null=True + roles = can_filter( + RuleSetSerializer( + source='rule_sets', many=True, read_only=True, allow_null=True + ), + name='role_detail', ) - users = UserSerializer( - source='user_set', many=True, read_only=True, allow_null=True + users = can_filter( + UserSerializer(source='user_set', many=True, read_only=True, allow_null=True), + name='user_detail', )