mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-30 12:26:25 -06:00
wip: Filter by custom fields in graph ql
This commit is contained in:
parent
05daa16aed
commit
3de7457a36
@ -27,7 +27,7 @@ from wireless.models import WirelessLAN, WirelessLink
|
|||||||
from .choices import *
|
from .choices import *
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import *
|
from .models import *
|
||||||
|
from django.db.models import JSONField
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CableFilterSet',
|
'CableFilterSet',
|
||||||
'CabledObjectFilterSet',
|
'CabledObjectFilterSet',
|
||||||
@ -1187,6 +1187,7 @@ class DeviceFilterSet(
|
|||||||
'inventory_item_count',
|
'inventory_item_count',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
|
import strawberry
|
||||||
|
import strawberry_django
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@ -10,20 +13,24 @@ from django.utils.translation import gettext as _
|
|||||||
from core.choices import ObjectChangeActionChoices
|
from core.choices import ObjectChangeActionChoices
|
||||||
from core.models import ObjectChange
|
from core.models import ObjectChange
|
||||||
from extras.choices import CustomFieldFilterLogicChoices
|
from extras.choices import CustomFieldFilterLogicChoices
|
||||||
|
from dcim.models import Device
|
||||||
|
|
||||||
from extras.filters import TagFilter
|
from extras.filters import TagFilter
|
||||||
from extras.models import CustomField, SavedFilter
|
from extras.models import CustomField, SavedFilter
|
||||||
from utilities.constants import (
|
from utilities.constants import (
|
||||||
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
|
FILTER_CHAR_BASED_LOOKUP_MAP,
|
||||||
FILTER_NUMERIC_BASED_LOOKUP_MAP
|
FILTER_NEGATION_LOOKUP_MAP,
|
||||||
|
FILTER_TREENODE_NEGATION_LOOKUP_MAP,
|
||||||
|
FILTER_NUMERIC_BASED_LOOKUP_MAP,
|
||||||
)
|
)
|
||||||
from utilities.forms.fields import MACAddressField
|
from utilities.forms.fields import MACAddressField
|
||||||
from utilities import filters
|
from utilities import filters
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BaseFilterSet',
|
"BaseFilterSet",
|
||||||
'ChangeLoggedModelFilterSet',
|
"ChangeLoggedModelFilterSet",
|
||||||
'NetBoxModelFilterSet',
|
"NetBoxModelFilterSet",
|
||||||
'OrganizationalModelFilterSet',
|
"OrganizationalModelFilterSet",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -31,58 +38,48 @@ __all__ = (
|
|||||||
# FilterSets
|
# FilterSets
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class BaseFilterSet(django_filters.FilterSet):
|
class BaseFilterSet(django_filters.FilterSet):
|
||||||
"""
|
"""
|
||||||
A base FilterSet which provides some enhanced functionality over django-filter2's FilterSet class.
|
A base FilterSet which provides some enhanced functionality over django-filter2's FilterSet class.
|
||||||
"""
|
"""
|
||||||
FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS)
|
|
||||||
FILTER_DEFAULTS.update({
|
FILTER_DEFAULTS = deepcopy(
|
||||||
models.AutoField: {
|
django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS
|
||||||
'filter_class': filters.MultiValueNumberFilter
|
)
|
||||||
},
|
FILTER_DEFAULTS.update(
|
||||||
models.CharField: {
|
{
|
||||||
'filter_class': filters.MultiValueCharFilter
|
models.AutoField: {"filter_class": filters.MultiValueNumberFilter},
|
||||||
},
|
models.CharField: {"filter_class": filters.MultiValueCharFilter},
|
||||||
models.DateField: {
|
models.DateField: {"filter_class": filters.MultiValueDateFilter},
|
||||||
'filter_class': filters.MultiValueDateFilter
|
models.DateTimeField: {
|
||||||
},
|
"filter_class": filters.MultiValueDateTimeFilter
|
||||||
models.DateTimeField: {
|
},
|
||||||
'filter_class': filters.MultiValueDateTimeFilter
|
models.DecimalField: {
|
||||||
},
|
"filter_class": filters.MultiValueDecimalFilter
|
||||||
models.DecimalField: {
|
},
|
||||||
'filter_class': filters.MultiValueDecimalFilter
|
models.EmailField: {"filter_class": filters.MultiValueCharFilter},
|
||||||
},
|
models.FloatField: {"filter_class": filters.MultiValueNumberFilter},
|
||||||
models.EmailField: {
|
models.IntegerField: {
|
||||||
'filter_class': filters.MultiValueCharFilter
|
"filter_class": filters.MultiValueNumberFilter
|
||||||
},
|
},
|
||||||
models.FloatField: {
|
models.PositiveIntegerField: {
|
||||||
'filter_class': filters.MultiValueNumberFilter
|
"filter_class": filters.MultiValueNumberFilter
|
||||||
},
|
},
|
||||||
models.IntegerField: {
|
models.PositiveSmallIntegerField: {
|
||||||
'filter_class': filters.MultiValueNumberFilter
|
"filter_class": filters.MultiValueNumberFilter
|
||||||
},
|
},
|
||||||
models.PositiveIntegerField: {
|
models.SlugField: {"filter_class": filters.MultiValueCharFilter},
|
||||||
'filter_class': filters.MultiValueNumberFilter
|
models.SmallIntegerField: {
|
||||||
},
|
"filter_class": filters.MultiValueNumberFilter
|
||||||
models.PositiveSmallIntegerField: {
|
},
|
||||||
'filter_class': filters.MultiValueNumberFilter
|
models.TimeField: {"filter_class": filters.MultiValueTimeFilter},
|
||||||
},
|
models.URLField: {"filter_class": filters.MultiValueCharFilter},
|
||||||
models.SlugField: {
|
MACAddressField: {
|
||||||
'filter_class': filters.MultiValueCharFilter
|
"filter_class": filters.MultiValueMACAddressFilter
|
||||||
},
|
},
|
||||||
models.SmallIntegerField: {
|
}
|
||||||
'filter_class': filters.MultiValueNumberFilter
|
)
|
||||||
},
|
|
||||||
models.TimeField: {
|
|
||||||
'filter_class': filters.MultiValueTimeFilter
|
|
||||||
},
|
|
||||||
models.URLField: {
|
|
||||||
'filter_class': filters.MultiValueCharFilter
|
|
||||||
},
|
|
||||||
MACAddressField: {
|
|
||||||
'filter_class': filters.MultiValueMACAddressFilter
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
def __init__(self, data=None, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
# bit of a hack for #9231 - extras.lookup.Empty is registered in apps.ready
|
# bit of a hack for #9231 - extras.lookup.Empty is registered in apps.ready
|
||||||
@ -91,11 +88,11 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
self.base_filters = self.__class__.get_filters()
|
self.base_filters = self.__class__.get_filters()
|
||||||
|
|
||||||
# Apply any referenced SavedFilters
|
# Apply any referenced SavedFilters
|
||||||
if data and ('filter' in data or 'filter_id' in data):
|
if data and ("filter" in data or "filter_id" in data):
|
||||||
data = data.copy() # Get a mutable copy
|
data = data.copy() # Get a mutable copy
|
||||||
saved_filters = SavedFilter.objects.filter(
|
saved_filters = SavedFilter.objects.filter(
|
||||||
Q(slug__in=data.pop('filter', [])) |
|
Q(slug__in=data.pop("filter", []))
|
||||||
Q(pk__in=data.pop('filter_id', []))
|
| Q(pk__in=data.pop("filter_id", []))
|
||||||
)
|
)
|
||||||
for sf in saved_filters:
|
for sf in saved_filters:
|
||||||
for key, value in sf.parameters.items():
|
for key, value in sf.parameters.items():
|
||||||
@ -113,36 +110,45 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_filter_lookup_dict(existing_filter):
|
def _get_filter_lookup_dict(existing_filter):
|
||||||
# Choose the lookup expression map based on the filter type
|
# Choose the lookup expression map based on the filter type
|
||||||
if isinstance(existing_filter, (
|
if isinstance(
|
||||||
django_filters.NumberFilter,
|
existing_filter,
|
||||||
filters.MultiValueDateFilter,
|
(
|
||||||
filters.MultiValueDateTimeFilter,
|
django_filters.NumberFilter,
|
||||||
filters.MultiValueNumberFilter,
|
filters.MultiValueDateFilter,
|
||||||
filters.MultiValueDecimalFilter,
|
filters.MultiValueDateTimeFilter,
|
||||||
filters.MultiValueTimeFilter
|
filters.MultiValueNumberFilter,
|
||||||
)):
|
filters.MultiValueDecimalFilter,
|
||||||
|
filters.MultiValueTimeFilter,
|
||||||
|
),
|
||||||
|
):
|
||||||
return FILTER_NUMERIC_BASED_LOOKUP_MAP
|
return FILTER_NUMERIC_BASED_LOOKUP_MAP
|
||||||
|
|
||||||
elif isinstance(existing_filter, (
|
elif isinstance(
|
||||||
filters.TreeNodeMultipleChoiceFilter,
|
existing_filter, (filters.TreeNodeMultipleChoiceFilter,)
|
||||||
)):
|
):
|
||||||
# TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression
|
# TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression
|
||||||
return FILTER_TREENODE_NEGATION_LOOKUP_MAP
|
return FILTER_TREENODE_NEGATION_LOOKUP_MAP
|
||||||
|
|
||||||
elif isinstance(existing_filter, (
|
elif isinstance(
|
||||||
django_filters.ModelChoiceFilter,
|
existing_filter,
|
||||||
django_filters.ModelMultipleChoiceFilter,
|
(
|
||||||
TagFilter
|
django_filters.ModelChoiceFilter,
|
||||||
)):
|
django_filters.ModelMultipleChoiceFilter,
|
||||||
|
TagFilter,
|
||||||
|
),
|
||||||
|
):
|
||||||
# These filter types support only negation
|
# These filter types support only negation
|
||||||
return FILTER_NEGATION_LOOKUP_MAP
|
return FILTER_NEGATION_LOOKUP_MAP
|
||||||
|
|
||||||
elif isinstance(existing_filter, (
|
elif isinstance(
|
||||||
django_filters.filters.CharFilter,
|
existing_filter,
|
||||||
django_filters.MultipleChoiceFilter,
|
(
|
||||||
filters.MultiValueCharFilter,
|
django_filters.filters.CharFilter,
|
||||||
filters.MultiValueMACAddressFilter
|
django_filters.MultipleChoiceFilter,
|
||||||
)):
|
filters.MultiValueCharFilter,
|
||||||
|
filters.MultiValueMACAddressFilter,
|
||||||
|
),
|
||||||
|
):
|
||||||
return FILTER_CHAR_BASED_LOOKUP_MAP
|
return FILTER_CHAR_BASED_LOOKUP_MAP
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@ -156,7 +162,10 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Skip nonstandard lookup expressions
|
# Skip nonstandard lookup expressions
|
||||||
if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'iexact', 'in']:
|
if (
|
||||||
|
existing_filter.method is not None
|
||||||
|
or existing_filter.lookup_expr not in ["exact", "iexact", "in"]
|
||||||
|
):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Choose the lookup expression map based on the filter type
|
# Choose the lookup expression map based on the filter type
|
||||||
@ -167,11 +176,14 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
|
|
||||||
# Get properties of the existing filter for later use
|
# Get properties of the existing filter for later use
|
||||||
field_name = existing_filter.field_name
|
field_name = existing_filter.field_name
|
||||||
|
|
||||||
|
# print("get_model_field", cls._meta.model, field_name)
|
||||||
|
|
||||||
field = get_model_field(cls._meta.model, field_name)
|
field = get_model_field(cls._meta.model, field_name)
|
||||||
|
|
||||||
# Create new filters for each lookup expression in the map
|
# Create new filters for each lookup expression in the map
|
||||||
for lookup_name, lookup_expr in lookup_map.items():
|
for lookup_name, lookup_expr in lookup_map.items():
|
||||||
new_filter_name = f'{existing_filter_name}__{lookup_name}'
|
new_filter_name = f"{existing_filter_name}__{lookup_name}"
|
||||||
existing_filter_extra = deepcopy(existing_filter.extra)
|
existing_filter_extra = deepcopy(existing_filter.extra)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -179,11 +191,14 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
# The filter field has been explicitly defined on the filterset class so we must manually
|
# The filter field has been explicitly defined on the filterset class so we must manually
|
||||||
# create the new filter with the same type because there is no guarantee the defined type
|
# create the new filter with the same type because there is no guarantee the defined type
|
||||||
# is the same as the default type for the field
|
# is the same as the default type for the field
|
||||||
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
|
|
||||||
|
resolve_field(
|
||||||
|
field, lookup_expr
|
||||||
|
) # Will raise FieldLookupError if the lookup is invalid
|
||||||
filter_cls = type(existing_filter)
|
filter_cls = type(existing_filter)
|
||||||
if lookup_expr == 'empty':
|
if lookup_expr == "empty":
|
||||||
filter_cls = django_filters.BooleanFilter
|
filter_cls = django_filters.BooleanFilter
|
||||||
for param_to_remove in ('choices', 'null_value'):
|
for param_to_remove in ("choices", "null_value"):
|
||||||
existing_filter_extra.pop(param_to_remove, None)
|
existing_filter_extra.pop(param_to_remove, None)
|
||||||
new_filter = filter_cls(
|
new_filter = filter_cls(
|
||||||
field_name=field_name,
|
field_name=field_name,
|
||||||
@ -191,21 +206,23 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
label=existing_filter.label,
|
label=existing_filter.label,
|
||||||
exclude=existing_filter.exclude,
|
exclude=existing_filter.exclude,
|
||||||
distinct=existing_filter.distinct,
|
distinct=existing_filter.distinct,
|
||||||
**existing_filter_extra
|
**existing_filter_extra,
|
||||||
)
|
)
|
||||||
elif hasattr(existing_filter, 'custom_field'):
|
elif hasattr(existing_filter, "custom_field"):
|
||||||
# Filter is for a custom field
|
# Filter is for a custom field
|
||||||
custom_field = existing_filter.custom_field
|
custom_field = existing_filter.custom_field
|
||||||
new_filter = custom_field.to_filter(lookup_expr=lookup_expr)
|
new_filter = custom_field.to_filter(lookup_expr=lookup_expr)
|
||||||
else:
|
else:
|
||||||
# The filter field is listed in Meta.fields so we can safely rely on default behaviour
|
# The filter field is listed in Meta.fields so we can safely rely on default behaviour
|
||||||
# Will raise FieldLookupError if the lookup is invalid
|
# Will raise FieldLookupError if the lookup is invalid
|
||||||
new_filter = cls.filter_for_field(field, field_name, lookup_expr)
|
new_filter = cls.filter_for_field(
|
||||||
|
field, field_name, lookup_expr
|
||||||
|
)
|
||||||
except FieldLookupError:
|
except FieldLookupError:
|
||||||
# The filter could not be created because the lookup expression is not supported on the field
|
# The filter could not be created because the lookup expression is not supported on the field
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if lookup_name.startswith('n'):
|
if lookup_name.startswith("n"):
|
||||||
# This is a negation filter which requires a queryset.exclude() clause
|
# This is a negation filter which requires a queryset.exclude() clause
|
||||||
# Of course setting the negation of the existing filter's exclude attribute handles both cases
|
# Of course setting the negation of the existing filter's exclude attribute handles both cases
|
||||||
new_filter.exclude = not existing_filter.exclude
|
new_filter.exclude = not existing_filter.exclude
|
||||||
@ -226,7 +243,11 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
|
|
||||||
additional_filters = {}
|
additional_filters = {}
|
||||||
for existing_filter_name, existing_filter in filters.items():
|
for existing_filter_name, existing_filter in filters.items():
|
||||||
additional_filters.update(cls.get_additional_lookups(existing_filter_name, existing_filter))
|
additional_filters.update(
|
||||||
|
cls.get_additional_lookups(
|
||||||
|
existing_filter_name, existing_filter
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
filters.update(additional_filters)
|
filters.update(additional_filters)
|
||||||
|
|
||||||
@ -234,8 +255,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter_for_lookup(cls, field, lookup_type):
|
def filter_for_lookup(cls, field, lookup_type):
|
||||||
|
if lookup_type == "empty":
|
||||||
if lookup_type == 'empty':
|
|
||||||
return django_filters.BooleanFilter, {}
|
return django_filters.BooleanFilter, {}
|
||||||
|
|
||||||
return super().filter_for_lookup(field, lookup_type)
|
return super().filter_for_lookup(field, lookup_type)
|
||||||
@ -245,31 +265,35 @@ class ChangeLoggedModelFilterSet(BaseFilterSet):
|
|||||||
"""
|
"""
|
||||||
Base FilterSet for ChangeLoggedModel classes.
|
Base FilterSet for ChangeLoggedModel classes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
created = filters.MultiValueDateTimeFilter()
|
created = filters.MultiValueDateTimeFilter()
|
||||||
last_updated = filters.MultiValueDateTimeFilter()
|
last_updated = filters.MultiValueDateTimeFilter()
|
||||||
created_by_request = django_filters.UUIDFilter(
|
created_by_request = django_filters.UUIDFilter(method="filter_by_request")
|
||||||
method='filter_by_request'
|
updated_by_request = django_filters.UUIDFilter(method="filter_by_request")
|
||||||
)
|
modified_by_request = django_filters.UUIDFilter(method="filter_by_request")
|
||||||
updated_by_request = django_filters.UUIDFilter(
|
|
||||||
method='filter_by_request'
|
|
||||||
)
|
|
||||||
modified_by_request = django_filters.UUIDFilter(
|
|
||||||
method='filter_by_request'
|
|
||||||
)
|
|
||||||
|
|
||||||
def filter_by_request(self, queryset, name, value):
|
def filter_by_request(self, queryset, name, value):
|
||||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||||
action = {
|
action = {
|
||||||
'created_by_request': Q(action=ObjectChangeActionChoices.ACTION_CREATE),
|
"created_by_request": Q(
|
||||||
'updated_by_request': Q(action=ObjectChangeActionChoices.ACTION_UPDATE),
|
action=ObjectChangeActionChoices.ACTION_CREATE
|
||||||
'modified_by_request': Q(action__in=[ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE]),
|
),
|
||||||
|
"updated_by_request": Q(
|
||||||
|
action=ObjectChangeActionChoices.ACTION_UPDATE
|
||||||
|
),
|
||||||
|
"modified_by_request": Q(
|
||||||
|
action__in=[
|
||||||
|
ObjectChangeActionChoices.ACTION_CREATE,
|
||||||
|
ObjectChangeActionChoices.ACTION_UPDATE,
|
||||||
|
]
|
||||||
|
),
|
||||||
}.get(name)
|
}.get(name)
|
||||||
request_id = value
|
request_id = value
|
||||||
pks = ObjectChange.objects.filter(
|
pks = ObjectChange.objects.filter(
|
||||||
action,
|
action,
|
||||||
changed_object_type=content_type,
|
changed_object_type=content_type,
|
||||||
request_id=request_id,
|
request_id=request_id,
|
||||||
).values_list('changed_object_id', flat=True)
|
).values_list("changed_object_id", flat=True)
|
||||||
return queryset.filter(pk__in=pks)
|
return queryset.filter(pk__in=pks)
|
||||||
|
|
||||||
|
|
||||||
@ -277,9 +301,10 @@ class NetBoxModelFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
"""
|
"""
|
||||||
Provides additional filtering functionality (e.g. tags, custom fields) for core NetBox models.
|
Provides additional filtering functionality (e.g. tags, custom fields) for core NetBox models.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method="search",
|
||||||
label=_('Search'),
|
label=_("Search"),
|
||||||
)
|
)
|
||||||
tag = TagFilter()
|
tag = TagFilter()
|
||||||
|
|
||||||
@ -289,19 +314,19 @@ class NetBoxModelFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
# Dynamically add a Filter for each CustomField applicable to the parent model
|
# Dynamically add a Filter for each CustomField applicable to the parent model
|
||||||
custom_fields = CustomField.objects.filter(
|
custom_fields = CustomField.objects.filter(
|
||||||
object_types=ContentType.objects.get_for_model(self._meta.model)
|
object_types=ContentType.objects.get_for_model(self._meta.model)
|
||||||
).exclude(
|
).exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED)
|
||||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
|
|
||||||
)
|
|
||||||
|
|
||||||
custom_field_filters = {}
|
custom_field_filters = {}
|
||||||
for custom_field in custom_fields:
|
for custom_field in custom_fields:
|
||||||
filter_name = f'cf_{custom_field.name}'
|
filter_name = f"cf_{custom_field.name}"
|
||||||
filter_instance = custom_field.to_filter()
|
filter_instance = custom_field.to_filter()
|
||||||
if filter_instance:
|
if filter_instance:
|
||||||
custom_field_filters[filter_name] = filter_instance
|
custom_field_filters[filter_name] = filter_instance
|
||||||
|
|
||||||
# Add relevant additional lookups
|
# Add relevant additional lookups
|
||||||
additional_lookups = self.get_additional_lookups(filter_name, filter_instance)
|
additional_lookups = self.get_additional_lookups(
|
||||||
|
filter_name, filter_instance
|
||||||
|
)
|
||||||
custom_field_filters.update(additional_lookups)
|
custom_field_filters.update(additional_lookups)
|
||||||
|
|
||||||
self.filters.update(custom_field_filters)
|
self.filters.update(custom_field_filters)
|
||||||
@ -317,11 +342,12 @@ class OrganizationalModelFilterSet(NetBoxModelFilterSet):
|
|||||||
"""
|
"""
|
||||||
A base class for adding the search method to models which only expose the `name` and `slug` fields
|
A base class for adding the search method to models which only expose the `name` and `slug` fields
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
models.Q(name__icontains=value) |
|
models.Q(name__icontains=value)
|
||||||
models.Q(slug__icontains=value) |
|
| models.Q(slug__icontains=value)
|
||||||
models.Q(description__icontains=value)
|
| models.Q(description__icontains=value)
|
||||||
)
|
)
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
|
|
||||||
|
from _decimal import Decimal
|
||||||
|
from datetime import date, datetime
|
||||||
from functools import partialmethod
|
from functools import partialmethod
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
import strawberry
|
import strawberry
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
|
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
from strawberry import auto
|
from strawberry import auto
|
||||||
|
|
||||||
|
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices
|
||||||
|
from extras.models import CustomField
|
||||||
from ipam.fields import ASNField
|
from ipam.fields import ASNField
|
||||||
from netbox.graphql.scalars import BigInt
|
from netbox.graphql.scalars import BigInt
|
||||||
from utilities.fields import ColorField, CounterCacheField
|
from utilities.fields import ColorField, CounterCacheField
|
||||||
@ -138,7 +145,7 @@ def autotype_decorator(filterset):
|
|||||||
class ExampleFilter(BaseFilterMixin):
|
class ExampleFilter(BaseFilterMixin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
The Filter itself must be derived from BaseFilterMixin. For items listed in meta.fields
|
The Filter itself must be derived from BaseFilterMixin. For items listed in meta.fields
|
||||||
of the filterset, usually just a type specifier is generated, so for
|
of the filterset, usually just a type specifier is generated, so for
|
||||||
`fields = [created, ]` the dataclass would be:
|
`fields = [created, ]` the dataclass would be:
|
||||||
|
|
||||||
@ -159,9 +166,70 @@ def autotype_decorator(filterset):
|
|||||||
setattr(cls, filter_name, partialmethod(filter_by_filterset, key=fieldname))
|
setattr(cls, filter_name, partialmethod(filter_by_filterset, key=fieldname))
|
||||||
|
|
||||||
def wrapper(cls):
|
def wrapper(cls):
|
||||||
|
|
||||||
|
# Dynamically add a Filter for each CustomField applicable to the parent model
|
||||||
|
custom_fields = CustomField.objects.filter(
|
||||||
|
object_types=ContentType.objects.get_for_model(filterset._meta.model)
|
||||||
|
).exclude(
|
||||||
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
|
||||||
|
)
|
||||||
|
|
||||||
|
## taken from netbox.filtersets.NetBoxModelFilterSet
|
||||||
|
custom_field_filters = {}
|
||||||
|
for custom_field in custom_fields:
|
||||||
|
filter_name = f'cf_{custom_field.name}'
|
||||||
|
filter_instance = custom_field.to_filter()
|
||||||
|
if filter_instance:
|
||||||
|
custom_field_filters[filter_name] = filter_instance
|
||||||
|
|
||||||
|
# Add relevant additional lookups
|
||||||
|
additional_lookups = filterset.get_additional_lookups(filter_name, filter_instance)
|
||||||
|
custom_field_filters.update(additional_lookups)
|
||||||
|
|
||||||
|
filterset.declared_filters.update(custom_field_filters)
|
||||||
|
|
||||||
|
## mirror the call to create_attribute_and_function for the model fields, but for custom fields of the model.
|
||||||
|
attr_type = None
|
||||||
|
match custom_field.type:
|
||||||
|
case CustomFieldTypeChoices.TYPE_TEXT:
|
||||||
|
attr_type = str | None
|
||||||
|
case CustomFieldTypeChoices.TYPE_LONGTEXT:
|
||||||
|
attr_type = str | None
|
||||||
|
case CustomFieldTypeChoices.TYPE_DATE:
|
||||||
|
attr_type = date | None
|
||||||
|
case CustomFieldTypeChoices.TYPE_DATETIME:
|
||||||
|
attr_type = datetime | None
|
||||||
|
case CustomFieldTypeChoices.TYPE_INTEGER:
|
||||||
|
attr_type = int | None
|
||||||
|
case CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||||
|
attr_type = bool | None
|
||||||
|
case CustomFieldTypeChoices.TYPE_DECIMAL:
|
||||||
|
attr_type = Decimal | None
|
||||||
|
|
||||||
|
#### unclear which types to choose here
|
||||||
|
# case CustomFieldTypeChoices.TYPE_JSON:
|
||||||
|
# attr_type = dict | None
|
||||||
|
# case [CustomFieldTypeChoices.TYPE_MULTIOBJECT]:
|
||||||
|
# attr_type = str | None
|
||||||
|
# case [CustomFieldTypeChoices.TYPE_MULTISELECT]:
|
||||||
|
# attr_type = str | None
|
||||||
|
# case [CustomFieldTypeChoices.TYPE_SELECT]:
|
||||||
|
# attr_type = str | None
|
||||||
|
# case [CustomFieldTypeChoices.TYPE_OBJECT]:
|
||||||
|
# attr_type = str | None
|
||||||
|
|
||||||
|
if attr_type:
|
||||||
|
create_attribute_and_function(
|
||||||
|
cls,
|
||||||
|
fieldname=filter_name,
|
||||||
|
attr_type=attr_type,
|
||||||
|
should_create_function=True
|
||||||
|
)
|
||||||
cls.filterset = filterset
|
cls.filterset = filterset
|
||||||
fields = filterset.get_fields()
|
fields = filterset.get_fields()
|
||||||
model = filterset._meta.model
|
model = filterset._meta.model
|
||||||
|
|
||||||
|
# Add a Filter for each field on the model itself
|
||||||
for fieldname in fields.keys():
|
for fieldname in fields.keys():
|
||||||
should_create_function = False
|
should_create_function = False
|
||||||
attr_type = auto
|
attr_type = auto
|
||||||
|
Loading…
Reference in New Issue
Block a user