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:
@@ -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."""
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user